diff --git a/kt/KT.md b/kt/KT.md index 134ec09..60fc976 100644 --- a/kt/KT.md +++ b/kt/KT.md @@ -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: + 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. diff --git a/kt/commands/checkout/command.py b/kt/commands/checkout/command.py index ef78518..b1fc656 100644 --- a/kt/commands/checkout/command.py +++ b/kt/commands/checkout/command.py @@ -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. """ @@ -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, ) diff --git a/kt/commands/checkout/impl.py b/kt/commands/checkout/impl.py index f855c64..28f1f49 100644 --- a/kt/commands/checkout/impl.py +++ b/kt/commands/checkout/impl.py @@ -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 diff --git a/kt/data/kernels.yaml b/kt/data/kernels.yaml index 2d58a09..5ed0dc3 100644 --- a/kt/data/kernels.yaml +++ b/kt/data/kernels.yaml @@ -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 diff --git a/kt/ktlib/config.py b/kt/ktlib/config.py index f9690e9..808fc41 100644 --- a/kt/ktlib/config.py +++ b/kt/ktlib/config.py @@ -34,6 +34,8 @@ class Config: ssh_key: Path user: str + use_worktrees: bool = True + DEFAULT: ClassVar = { "base_path": "~/ciq", "kernels_dir": "~/ciq/kernels", @@ -41,6 +43,7 @@ class Config: "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"} @@ -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) diff --git a/kt/ktlib/kernel_workspace.py b/kt/ktlib/kernel_workspace.py index d18a3f1..ed6fd75 100644 --- a/kt/ktlib/kernel_workspace.py +++ b/kt/ktlib/kernel_workspace.py @@ -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): @@ -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(), ) + @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 --cleanup\n" + f" kt checkout --{'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( @@ -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) + + except GitCommandError as e: + self.cleanup() + raise e + def update(self): """ It will make sure the worktree is up-to-date with remote. @@ -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: + 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") @@ -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 @@ -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) @@ -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( diff --git a/kt/ktlib/kernels.py b/kt/ktlib/kernels.py index 2fe6882..ddb6aad 100644 --- a/kt/ktlib/kernels.py +++ b/kt/ktlib/kernels.py @@ -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 diff --git a/tests/kt/ktlib/test_config.py b/tests/kt/ktlib/test_config.py index 3e1de18..a3adbc2 100644 --- a/tests/kt/ktlib/test_config.py +++ b/tests/kt/ktlib/test_config.py @@ -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 diff --git a/tests/kt/ktlib/test_kernel_workspace.py b/tests/kt/ktlib/test_kernel_workspace.py new file mode 100644 index 0000000..7554814 --- /dev/null +++ b/tests/kt/ktlib/test_kernel_workspace.py @@ -0,0 +1,347 @@ +import pytest +from git import Repo +from pathlib3x import Path + +from kt.ktlib.kernel_workspace import KernelWorkspace, RepoWorktree +from kt.ktlib.util import Constants + + +@pytest.fixture +def git_topology(tmp_path): + """ + Build the topology that mirrors production: + bare_remote — the "real" upstream (like github.com/ctrliq/kernel-src-tree) + source_root — a clone of bare_remote (what `kt setup` creates) + The kernel branch exists in bare_remote but in source_root only as + refs/remotes/origin/. + """ + tmp_path = Path(str(tmp_path)) + bare_remote = tmp_path / "bare_remote.git" + Repo.init(str(bare_remote), bare=True) + bare_repo = Repo(str(bare_remote)) + + source_root_path = tmp_path / "source_root" + source_root_repo = Repo.clone_from(str(bare_remote), str(source_root_path)) + + # Create an initial commit on main in source_root, push to bare + readme = source_root_path / "README" + readme.write_text("init") + source_root_repo.index.add(["README"]) + source_root_repo.index.commit("initial commit") + source_root_repo.remotes.origin.push("HEAD:refs/heads/main") + + # Create the kernel branch on the bare remote directly + kernel_branch = "ciqcbr7_9" + bare_repo.git.branch(kernel_branch, "main") + + # Fetch so source_root has origin/ciqcbr7_9 + source_root_repo.remotes.origin.fetch() + + kernels_dir = tmp_path / "kernels" + kernels_dir.mkdir() + + return { + "bare_remote": bare_remote, + "source_root_path": source_root_path, + "source_root_repo": source_root_repo, + "kernels_dir": kernels_dir, + "kernel_branch": kernel_branch, + "tmp_path": tmp_path, + } + + +def _make_repo_worktree(git_topology, folder_name, use_worktree): + """Helper to create a RepoWorktree with the test topology.""" + folder = git_topology["kernels_dir"] / folder_name + return RepoWorktree( + source_root=git_topology["source_root_repo"], + folder=folder, + remote="origin", + remote_branch=git_topology["kernel_branch"], + local_branch=f"{{testuser}}_{git_topology['kernel_branch']}", + use_worktree=use_worktree, + ) + + +# --- detect_repo_disk_mode --- + + +def test_detect_disk_mode_worktree(git_topology): + rw = _make_repo_worktree(git_topology, "wt_detect", use_worktree=True) + rw.setup() + assert RepoWorktree.detect_repo_disk_mode(rw.folder) is True + + +def test_detect_disk_mode_clone(git_topology): + rw = _make_repo_worktree(git_topology, "cl_detect", use_worktree=False) + rw.setup() + assert RepoWorktree.detect_repo_disk_mode(rw.folder) is False + + +def test_detect_disk_mode_nonexistent(tmp_path): + assert RepoWorktree.detect_repo_disk_mode(Path(str(tmp_path)) / "nope") is None + + +# --- Clone setup --- + + +def test_clone_setup_git_dir_is_directory(git_topology): + rw = _make_repo_worktree(git_topology, "clone_test", use_worktree=False) + rw.setup() + git_path = rw.folder / ".git" + assert git_path.is_dir() + + +def test_clone_setup_local_branch_tracks_remote(git_topology): + rw = _make_repo_worktree(git_topology, "clone_track", use_worktree=False) + rw.setup() + repo = Repo(str(rw.folder)) + assert repo.active_branch.name == f"{{testuser}}_{git_topology['kernel_branch']}" + tracking = repo.active_branch.tracking_branch() + assert tracking is not None + assert git_topology["kernel_branch"] in str(tracking) + + +def test_clone_setup_origin_is_real_remote(git_topology): + rw = _make_repo_worktree(git_topology, "clone_origin", use_worktree=False) + rw.setup() + repo = Repo(str(rw.folder)) + origin_url = repo.remotes.origin.url + assert origin_url == str(git_topology["bare_remote"]) + + +# --- Clone idempotency --- + + +def test_clone_idempotency_update(git_topology): + rw = _make_repo_worktree(git_topology, "clone_idem", use_worktree=False) + rw.setup() + + # Make a new commit on bare remote + source = git_topology["source_root_repo"] + readme = git_topology["source_root_path"] / "README" + readme.write_text("updated") + source.index.add(["README"]) + source.index.commit("second commit") + source.remotes.origin.push(f"HEAD:refs/heads/{git_topology['kernel_branch']}") + + # Re-run setup — should update, not error + rw.setup() + assert "updated" in (rw.folder / "README").read_text() + + +# --- Clone cleanup --- + + +def test_clone_cleanup_removes_folder(git_topology): + rw = _make_repo_worktree(git_topology, "clone_clean", use_worktree=False) + rw.setup() + assert rw.folder.exists() + rw.cleanup() + assert not rw.folder.exists() + + +# --- Clone failure path --- + + +def test_clone_failure_cleans_up_partial(git_topology): + rw = _make_repo_worktree(git_topology, "clone_fail", use_worktree=False) + # Break the remote branch so checkout fails + rw.remote_branch = "nonexistent_branch" + with pytest.raises(Exception): # noqa: B017 + rw.setup() + # Partial folder should be cleaned up + assert not rw.folder.exists() + + +# --- Worktree failure path --- + + +def test_worktree_failure_cleans_up_partial(git_topology): + rw = _make_repo_worktree(git_topology, "wt_fail", use_worktree=True) + rw.remote_branch = "nonexistent_branch" + with pytest.raises(Exception): # noqa: B017 + rw.setup() + assert not rw.folder.exists() + + +# --- Worktree setup --- + + +def test_worktree_setup_git_is_file(git_topology): + rw = _make_repo_worktree(git_topology, "wt_test", use_worktree=True) + rw.setup() + git_path = rw.folder / ".git" + assert git_path.is_file() + + +def test_worktree_setup_branch_in_source_root(git_topology): + rw = _make_repo_worktree(git_topology, "wt_branch", use_worktree=True) + rw.setup() + branches = [h.name for h in git_topology["source_root_repo"].heads] + assert rw.local_branch in branches + + +def test_worktree_cleanup_removes_both(git_topology): + rw = _make_repo_worktree(git_topology, "wt_clean", use_worktree=True) + rw.setup() + assert rw.folder.exists() + rw.cleanup() + assert not rw.folder.exists() + branches = [h.name for h in git_topology["source_root_repo"].heads] + assert rw.local_branch not in branches + + +# --- Mode mismatch --- + + +def test_mode_mismatch_clone_then_worktree(git_topology): + rw_clone = _make_repo_worktree(git_topology, "mismatch1", use_worktree=False) + rw_clone.setup() + rw_wt = _make_repo_worktree(git_topology, "mismatch1", use_worktree=True) + with pytest.raises(RuntimeError, match="Mode mismatch"): + rw_wt.setup() + + +def test_mode_mismatch_worktree_then_clone(git_topology): + rw_wt = _make_repo_worktree(git_topology, "mismatch2", use_worktree=True) + rw_wt.setup() + rw_clone = _make_repo_worktree(git_topology, "mismatch2", use_worktree=False) + with pytest.raises(RuntimeError, match="Mode mismatch"): + rw_clone.setup() + + +# --- load_from_filepath detection --- + + +def test_load_from_filepath_clone(git_topology): + rw = _make_repo_worktree(git_topology, "load_clone", use_worktree=False) + rw.setup() + loaded = RepoWorktree.load_from_filepath(rw.folder) + assert loaded.use_worktree is False + + +def test_load_from_filepath_worktree(git_topology): + rw = _make_repo_worktree(git_topology, "load_wt", use_worktree=True) + rw.setup() + loaded = RepoWorktree.load_from_filepath(rw.folder) + assert loaded.use_worktree is True + + +# =================================================================== +# Integration-style tests matching user's requested scenarios +# =================================================================== + + +def _make_full_workspace(git_topology, name, use_worktree): + """Build a KernelWorkspace with both dist and src sub-repos.""" + folder = git_topology["kernels_dir"] / name + branch = git_topology["kernel_branch"] + source_root = git_topology["source_root_repo"] + + dist_worktree = RepoWorktree( + source_root=source_root, + folder=folder / Constants.DIST_TREE, + remote="origin", + remote_branch=branch, + local_branch=f"{{testuser}}_{branch}_dist", + use_worktree=use_worktree, + ) + src_worktree = RepoWorktree( + source_root=source_root, + folder=folder / Constants.SRC_TREE, + remote="origin", + remote_branch=branch, + local_branch=f"{{testuser}}_{branch}_src", + use_worktree=use_worktree, + ) + return KernelWorkspace( + folder=folder, + dist_worktree=dist_worktree, + src_worktree=src_worktree, + ) + + +# --- cbr-7.9 default (use_worktree: false in yaml) --- + + +def test_cbr79_default_creates_clones(git_topology): + ws = _make_full_workspace(git_topology, "cbr-7.9", use_worktree=False) + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_dir() + + ws.cleanup() + assert not ws.folder.exists() + + +# --- cbr-7.9 --worktree (user explicitly overrides to worktree) --- + + +def test_cbr79_worktree_override_creates_worktrees(git_topology): + ws = _make_full_workspace(git_topology, "cbr-7.9-wt", use_worktree=True) + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_file() + + ws.cleanup() + assert not ws.folder.exists() + + +# --- lts-9.2 default (no use_worktree in yaml -> worktree mode) then checkout again --- + + +def test_lts92_default_worktree_then_update(git_topology): + ws = _make_full_workspace(git_topology, "lts-9.2", use_worktree=True) + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_file() + + # Second checkout — should update without error + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_file() + + ws.cleanup() + assert not ws.folder.exists() + + +# --- lts-9.2 --no-worktree then lts-9.2 --worktree (mode switch) --- + + +def test_lts92_no_worktree_then_worktree_mismatch(git_topology): + # First checkout with --no-worktree + ws_clone = _make_full_workspace(git_topology, "lts-9.2-switch", use_worktree=False) + ws_clone.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws_clone.folder / sub + assert (sub_path / ".git").is_dir() + + # Second checkout with --worktree — should error on mismatch + ws_wt = _make_full_workspace(git_topology, "lts-9.2-switch", use_worktree=True) + with pytest.raises(RuntimeError, match="Mode mismatch"): + ws_wt.setup() + + # After cleanup + re-checkout, worktree mode works + ws_clone.cleanup() + ws_wt.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws_wt.folder / sub + assert (sub_path / ".git").is_file() + + ws_wt.cleanup() + assert not ws_wt.folder.exists() diff --git a/tests/kt/ktlib/test_kernels.py b/tests/kt/ktlib/test_kernels.py index 9370929..1d95e07 100644 --- a/tests/kt/ktlib/test_kernels.py +++ b/tests/kt/ktlib/test_kernels.py @@ -163,3 +163,98 @@ def test_kernels_vm_image_url_present(): kernels_info = KernelsInfo.from_dict(data=data_with_pin, private_data={}, config=config) kernel_info = list(kernels_info.kernels.values())[0] assert kernel_info.vm_image_url == pinned_url + + +# --- use_worktree field --- + + +def test_kernels_use_worktree_default_none(): + config = Config.from_str_dict(Config.DEFAULT) + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.use_worktree is None + + +def test_kernels_use_worktree_parsed_from_dict(): + kernels_with_flag = { + "kernel1": { + **kernels["kernel1"], + "use_worktree": False, + } + } + data_with_flag = {"common_repos": common_repos, "kernels": kernels_with_flag} + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data_with_flag, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.use_worktree is False + + +def test_kernels_use_worktree_settable_via_overrides(): + config = Config.from_str_dict(Config.DEFAULT) + overrides = {"kernel1": {"use_worktree": False}} + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config, kernel_overrides=overrides) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.use_worktree is False + + +# --- should_use_worktree precedence --- + + +def test_should_use_worktree_cli_wins(): + config = Config.from_str_dict({**Config.DEFAULT, "use_worktrees": True}) + kernels_with_flag = {"kernel1": {**kernels["kernel1"], "use_worktree": True}} + data_with_flag = {"common_repos": common_repos, "kernels": kernels_with_flag} + + kernels_info = KernelsInfo.from_dict(data=data_with_flag, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.should_use_worktree(config=config, cli_value=False) is False + + +def test_should_use_worktree_per_kernel_over_global(): + config = Config.from_str_dict({**Config.DEFAULT, "use_worktrees": True}) + kernels_with_flag = {"kernel1": {**kernels["kernel1"], "use_worktree": False}} + data_with_flag = {"common_repos": common_repos, "kernels": kernels_with_flag} + + kernels_info = KernelsInfo.from_dict(data=data_with_flag, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.should_use_worktree(config=config) is False + + +def test_should_use_worktree_global_fallback(): + config = Config.from_str_dict({**Config.DEFAULT, "use_worktrees": False}) + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + # kernel1 has no use_worktree set, so falls through to global + assert kernel_info.should_use_worktree(config=config) is False + + +def test_should_use_worktree_default_true(): + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.should_use_worktree(config=config) is True + + +def test_should_use_worktree_centos7_warns(caplog): + kernels_centos7 = { + "kernel1": { + **kernels["kernel1"], + "os_variant": "centos7", + } + } + data_centos7 = {"common_repos": common_repos, "kernels": kernels_centos7} + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data_centos7, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + + import logging + + with caplog.at_level(logging.WARNING): + result = kernel_info.should_use_worktree(config=config) + assert result is True + assert "CentOS 7" in caplog.text