Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions kt/KT.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,46 @@ kernel_overrides:
```bash
$ kt setup
```
### Disable Worktrees
By default, kt will use git worktrees to checkout the kernel source and
appropriate dist-git repo. If you don't want to use worktrees you have several
options. Each option below will override the previous one, so if you turn off
worktrees at the kt config level you can turn them on for specific kernels later
by setting in the .private_repos.yaml or on the command line.

NOTE: By default CentOS7 worktree is turned off due to git inside the CentOS7
VM is too old to support worktrees. You can enable it if you want, but note
that this is a limitation of the VM.

1. Set the global config variable.
```bash
{
"base_path": "~/workspace/kt_test",
"kernels_dir": "~/workspace/kt_test/kernels",
"images_source_dir": "~/workspace/kt_test/images_source",
"images_dir": "~/workspace/kt_test/images",
"ssh_key": "~/.ssh/test.pub",
"user": "USER",
"use_worktrees": false
}
```

2. Set the per kernel config in the .private_repos.yaml file.
```yaml
kernel_overrides:
lts-9.2:
dist_git_branch: <private_branch>
dist_git_root: dist-git-tree-lts
use_worktree: false
```

3. CLI command line override. This will override the global and per kernel
config. However you will need to use this option for every command that uses
every time for that kernel version.
```bash
$ kt checkout lts-9.2 --no-worktree
```

## Implementation details:

kt/ktlib is the place for common helpers that would be used for kt commands.
Expand Down
13 changes: 12 additions & 1 deletion kt/commands/checkout/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
\b
$ kt checkout lts-9.2 -e CVE-2022-49909
Will create folder lts-9.2_CVE-2022-49909 instead of lts-9.2.
\b
$ kt checkout cbr-7.9 --no-worktree
Will not create worktrees for CentOS7 bridge.
Note: This is recommended because the git version in the CentOS7 VM is too old to support worktrees.
"""


Expand All @@ -61,12 +65,19 @@
type=str,
help="Feature you'll be working on",
)
@click.option(
"--worktree/--no-worktree",
"use_worktree",
default=None,
help="Manually override config worktree configuration for kernel.",
)
@click.argument("kernel", required=True, type=str, shell_complete=ShellCompletion.show_kernels)
def checkout(kernel, change_dir, override, cleanup, extra):
def checkout(kernel, change_dir, override, cleanup, extra, use_worktree):
main(
name=kernel,
change_dir=change_dir,
override=override,
cleanup=cleanup,
use_worktree=use_worktree,
extra=extra,
)
7 changes: 5 additions & 2 deletions kt/commands/checkout/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
from kt.ktlib.kernels import KernelsInfo


def main(name: str, change_dir: bool, cleanup: bool, override: bool, extra: str):
def main(name: str, change_dir: bool, cleanup: bool, override: bool, use_worktree: bool | None, extra: str):
config = Config.load()
kernels = KernelsInfo.from_yaml(config=config).kernels
if name not in kernels:
raise ValueError(f"Invalid param: {name} does not exist")

kernel_info = kernels[name]
kernel_workspace = KernelWorkspace.load(name=name, config=config, kernel_info=kernel_info, extra=extra)
resolved = kernel_info.should_use_worktree(config=config, cli_value=use_worktree)
kernel_workspace = KernelWorkspace.load(
name=name, config=config, kernel_info=kernel_info, use_worktree=resolved, extra=extra
)
if cleanup:
kernel_workspace.cleanup()
return
Expand Down
1 change: 1 addition & 0 deletions kt/data/kernels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ kernels:
vm_image_url: https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2211.qcow2
os_variant: centos7
use_nfs: true
use_worktree: false

lts-8.6:
src_tree_root: kernel-src-tree
Expand Down
9 changes: 8 additions & 1 deletion kt/ktlib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ class Config:
ssh_key: Path
user: str

use_worktrees: bool = True

DEFAULT: ClassVar = {
"base_path": "~/ciq",
"kernels_dir": "~/ciq/kernels",
"images_source_dir": "~/ciq/default_test_images",
"images_dir": "~/ciq/tmp/virt-images",
"ssh_key": "~/.ssh/id_ed25519_generic.pub",
"user": os.environ["USER"],
"use_worktrees": True,
}

REQUIRED_KEYS: ClassVar = {"base_path", "kernels_dir", "images_source_dir", "images_dir", "ssh_key"}
Expand All @@ -59,10 +62,14 @@ def from_str_dict(cls, data: dict[str, str]):
data = {**data, "user": os.environ["USER"]}

# Transform the str values to Path except for user
non_path_keys = {"user"}
non_path_keys = {"user", "use_worktrees"}
new_data = {k: (Path(v).expanduser() if k not in non_path_keys else v) for k, v in data.items()}
if not all(v.is_absolute() for k, v in new_data.items() if k not in non_path_keys):
raise ValueError("all paths should be absolute; check your config")
use_worktrees = new_data.get("use_worktrees", None)
if use_worktrees is not None:
if not isinstance(use_worktrees, bool):
raise ValueError(f"use_worktrees expected Boolean value got [{use_worktrees}]")

return cls(**new_data)

Expand Down
67 changes: 64 additions & 3 deletions kt/ktlib/kernel_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class RepoWorktree:
remote: str
remote_branch: str
local_branch: str
use_worktree: bool = True

@classmethod
def load_from_filepath(cls, folder: Path):
Expand All @@ -38,14 +39,51 @@ def load_from_filepath(cls, folder: Path):
remote=remote,
remote_branch=remote_branch,
local_branch=local_branch,
use_worktree=(folder / ".git").is_file(),
)
Comment thread
PlaidCat marked this conversation as resolved.

@staticmethod
def detect_repo_disk_mode(directory) -> bool | None:
"""
Static method to work out if a .git exists in directory.
If the .git is a file its a part of a worktree representation
else a directory is a pure clone.
Return
True - Git WorkTree
False - Git Clone Repo
None - NOT a git repo
"""
git_path = directory / ".git"
if git_path.is_file():
return True
if git_path.is_dir():
return False
return None

def setup(self):
"""
First run: It will create the worktree
Second run: It will update the worktree
First run: It will create the worktree / clone
Second run: It will update the worktree / clone
"""

if self.folder.exists():
disk_mode = self.detect_repo_disk_mode(self.folder)
if disk_mode is None or disk_mode != self.use_worktree:
disk_label = "worktree" if disk_mode else "clone" if disk_mode is False else "unknown"
requested_label = "worktree" if self.use_worktree else "clone"
raise RuntimeError(
f"Mode mismatch for {self.folder}:\n"
f" On disk: {disk_label}\n"
f" Requested: {requested_label}\n"
"To switch modes, clean up first then re-checkout:\n"
f" kt checkout <kernel> --cleanup\n"
f" kt checkout <kernel> --{'worktree' if self.use_worktree else 'no-worktree'}"
)
self.update()
return
self._setup_worktree() if self.use_worktree else self._setup_clone()

def _setup_worktree(self):
try:
remote_ref = f"{self.remote}/{self.remote_branch}"
self.source_root.git.worktree(
Expand All @@ -65,6 +103,18 @@ def setup(self):
self.cleanup()
raise e

def _setup_clone(self):
try:
logging.info(f"Cloning Full Repo {self.source_root.remotes.origin.url} to {self.folder}")
repo = Repo.clone_from(url=self.source_root.remotes.origin.url, to_path=self.folder, no_checkout=True)
repo_ref = f"origin/{self.remote_branch}"
logging.info(f"Checking out {self.local_branch} tracking to {repo_ref}")
repo.git.checkout("-b", self.local_branch, "--track", repo_ref)
Comment thread
PlaidCat marked this conversation as resolved.

except GitCommandError as e:
self.cleanup()
raise e

def update(self):
"""
It will make sure the worktree is up-to-date with remote.
Expand All @@ -76,6 +126,15 @@ def update(self):
repo.remotes.origin.pull(rebase=True)

def cleanup(self):
disk_mode = self.detect_repo_disk_mode(self.folder)
if disk_mode is True:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the checkout was originally a worktree, and someone manually deleted the worktree, and then did kt checkout lts-X.Y --cleanup _cleanup_worktree() wouldn't get called. Which is fine wrt the worktree itself, but the local branch in the source_root wouldn't get cleaned up. Prior to this change, even if the worktree got manually deleted, the local branch in the source_root would get cleaned up. Sort of a corner case so maybe not a big deal. I'm not sure how you'd fix it actually. You don't want to unconditionally delete a branch with the same name from the source_root if the original checkout was a clone. You might be deleting an unrelated branch that just happened to have the same name. We may just have to live with the orphaned branch if the worktree is deleted.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah but the counter point could also be true previously if you did that then made a same named branch in the source_root you'd end up unconditionally deleting a branch in the source_root.

worktrees are weird and obfuscation tools run into weird multip workflow issues.

I think we'll leave it and if it becomes a wide problem then we can address it then as there are a couple extreme edge and optimizations are not implemented in here to begin with.

self._cleanup_worktree()
return

logging.info(f"Removing Local Clone for {self.folder}")
self.folder.rmtree(ignore_errors=True)

def _cleanup_worktree(self):
# remove worktree, only if it exists
try:
self.source_root.git.worktree("remove", self.folder, "-f")
Expand Down Expand Up @@ -165,7 +224,7 @@ def load_from_name(cls, kernel_workspace_name: str):
return workspace

@classmethod
def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str):
def load(cls, name: str, config: Config, kernel_info: KernelInfo, use_worktree: bool, extra: str):
if extra:
name = name + "_" + extra

Expand All @@ -184,6 +243,7 @@ def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str):
remote=default_remote,
remote_branch=kernel_info.dist_git_branch,
local_branch=dist_local_branch,
use_worktree=use_worktree,
)

src_folder = folder / Path(Constants.SRC_TREE)
Expand All @@ -197,6 +257,7 @@ def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str):
remote=default_remote,
remote_branch=kernel_info.src_tree_branch,
local_branch=src_local_branch,
use_worktree=use_worktree,
)

return cls(
Expand Down
20 changes: 20 additions & 0 deletions kt/ktlib/kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ class KernelInfo:
os_variant: str | None = None
use_nfs: bool = False
overridden: bool = False
use_worktree: bool | None = None

def should_use_worktree(self, config: Config, cli_value=None) -> bool:
ret = True
if cli_value is not None:
ret = cli_value
elif self.use_worktree is not None:
ret = self.use_worktree
else:
ret = config.use_worktrees

if self.os_variant == "centos7" and ret is True:
logging.warning(
"CentOS 7 does not support worktrees internally.\n"
"Please check your configs:\n"
f"- cli_value: {cli_value}\n"
f"- local use_worktree: {self.use_worktree}\n"
f"- config value: {config.use_worktrees}"
)
return ret


@dataclass
Expand Down
52 changes: 52 additions & 0 deletions tests/kt/ktlib/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,55 @@ def test_config_load_from_json_proper_ssh_key():
def test_config_load_from_json_proper_user():
config = Config.from_json(CONFIG_STR)
assert config.user == "testuser"


def test_config_use_worktrees_default_true():
config = Config.from_json(CONFIG_STR)
assert config.use_worktrees is True


def test_config_use_worktrees_false():
json_data = (
"{"
'"base_path": "~/ciq",'
'"kernels_dir": "~/ciq/kernels",'
'"images_source_dir": "~/ciq/default_test_images",'
'"images_dir": "~/ciq/tmp/virt-images",'
'"ssh_key": "~/ciq/id_ed25519_generic.pub",'
'"user": "testuser",'
'"use_worktrees": false'
"}"
)
config = Config.from_json(json_data)
assert config.use_worktrees is False


def test_config_use_worktrees_non_bool_raises():
json_data = (
"{"
'"base_path": "~/ciq",'
'"kernels_dir": "~/ciq/kernels",'
'"images_source_dir": "~/ciq/default_test_images",'
'"images_dir": "~/ciq/tmp/virt-images",'
'"ssh_key": "~/ciq/id_ed25519_generic.pub",'
'"user": "testuser",'
'"use_worktrees": "false"'
"}"
)
with pytest.raises(ValueError, match="use_worktrees"):
Config.from_json(json_data)


def test_config_use_worktrees_not_required():
json_data = (
"{"
'"base_path": "~/ciq",'
'"kernels_dir": "~/ciq/kernels",'
'"images_source_dir": "~/ciq/default_test_images",'
'"images_dir": "~/ciq/tmp/virt-images",'
'"ssh_key": "~/ciq/id_ed25519_generic.pub",'
'"user": "testuser"'
"}"
)
config = Config.from_json(json_data)
assert config.use_worktrees is True
Loading