Skip to content

Commit a64d0d8

Browse files
committed
kt: Add checkout command
Usage: kt checkout <kernel> This will set up the working directory for a kernel. It will create git worktrees for dist-git-tree and kernel-src-tree repos. If used if the working directory for a kernel is created, the two git worktrees are updated. An --extra option was added if people need 2 working environments for the same kernel at the same time. Example without --extra: $ kt checkout lts9-2 Output: $KERNELS_DIR/lts-9.2 will be created with 2 git worktrees: kernel-src-tree -- for branch {USER}/ciqlts9_2 kernel-dist-git -- for branch {USER}/ciqlts9_2 Example with --extra: $ kt checkout lts9-2 --extra CVE_X Output: $KERNELS_DIR/lts-9.2_CVE_X will be created with 2 git worktrees: kernel-src-tree -- for branch {USER}/ciqlts9_2_CVE_X kernel-dist-git -- for branch {USER}/ciqlts9_2_CVE_X TODO: Autocomplete suggests only the kernel in data/kernels.yaml, not the kernel workspace at the moment. This will be improved later, since kt checkout may be used to just update en existing kernel workspace. Signed-off-by: Roxana Nicolescu <rnicolescu@ciq.com>
1 parent 7eb73d3 commit a64d0d8

7 files changed

Lines changed: 356 additions & 0 deletions

File tree

bin/kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import logging
44

55
import click
66

7+
from kt.commands.checkout.command import checkout
78
from kt.commands.list_kernels.command import list_kernels
89
from kt.commands.setup.command import setup
910

@@ -24,6 +25,7 @@ def main():
2425

2526
cli.add_command(list_kernels)
2627
cli.add_command(setup)
28+
cli.add_command(checkout)
2729
cli()
2830

2931

kt/KT.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,58 @@ If config.base_path = ~/ciq, these will be created:
172172

173173
If there's a repo that needs to be cloned relevant for any future command,
174174
this is when it should be cloned.
175+
176+
### kt checkout
177+
Prepares the working directory for a kernel.
178+
It uses the KernelInfo dataclass created based on the kernels.yaml file.
179+
180+
The working directory location is based on configuration:
181+
`<config.kernels_dir>/<kernel>/`
182+
183+
2 git worktrees are created for a kernel:
184+
185+
- kernel-dist-git
186+
187+
- kernel-src-tree
188+
189+
They will point out to their root sources. Check kt setup for more info.
190+
They should be located in <config.base_path>.
191+
The worktrees reference the remote <branch> from kernels.yaml.
192+
The local branch is `{<user>}/<branch>`.
193+
194+
If `--change-dir` or `-c` option is used, it will also go to the working
195+
directory of the kernel.
196+
197+
If `--cleanup` option is used, it will delete the worktree and the local branch
198+
before creating it from scratch again.
199+
200+
#### Example:
201+
```
202+
$ kt checkout lts9_4
203+
```
204+
205+
For this configuration
206+
```
207+
{
208+
"base_path": "~/ciq",
209+
"kernels_dir": "~/ciq/kernels",
210+
"images_source_dir": "~/ciq/default_test_images",
211+
"images_dir": "~/ciq/tmp/virt-images",
212+
"ssh_key": "~/.ssh/id_ed25519_generic.pub",
213+
}
214+
```
215+
216+
This is the working directory for this kernel:
217+
`~/ciq/kernels/lts9_4`.
218+
219+
2 git worktrees are created:
220+
221+
1. kernel-dist-git
222+
223+
This representes branch `{<user>}/lts9_4:origin/lts9_4`.
224+
The source repo is ~/ciq/dist-git-tree-lts
225+
226+
2. kernel-src-tree
227+
228+
This representes branch `{<user>}/ciqlts9_4:origin/ciqlts9_4`
229+
The source repo is ~/ciq/kernel-src-tree

kt/commands/checkout/command.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import click
2+
3+
from kt.commands.checkout.impl import main
4+
from kt.ktlib.shell_completion import ShellCompletion
5+
6+
epilog = """
7+
Prepares the working directory for a kernel.
8+
It uses the KernelInfo dataclass created based on the kernels.yaml file.
9+
10+
The working directory location is based on configuration:
11+
<config.kernels_dir>/<kernel>/
12+
13+
2 git worktrees are created for a kernel:
14+
15+
- dist-git-tree
16+
17+
- kernel-src-tree
18+
19+
They will point out to their root sources. Check kt setup for more info.
20+
They should be located in <config.base_path>.
21+
22+
The worktrees reference the remote <branch> from kernels.yaml.
23+
The local branch is {<user>}/<branch>.
24+
25+
Examples:
26+
27+
\b
28+
$ kt checkout lts9_4
29+
\b
30+
$ kt checkout lts9_4 --cleanup
31+
\b
32+
$ kt checkout lts9_4 --cleanup -c
33+
\b
34+
$ kt checkout lts9_4 --cleanup -change-dir
35+
\b
36+
$ kt checkout lts9_4 -e CVE-2022-49909
37+
Will create folder lts9_4_CVE-2022-49909 instead of lts9_4.
38+
"""
39+
40+
41+
@click.command(epilog=epilog)
42+
@click.option(
43+
"-c",
44+
"--change-dir",
45+
is_flag=True,
46+
help="Change directory to the kernel directory",
47+
)
48+
@click.option(
49+
"--override",
50+
is_flag=True,
51+
help="Delete existing worktree for the kernel and start again",
52+
)
53+
@click.option(
54+
"--cleanup",
55+
is_flag=True,
56+
help="Delete existing worktree for the kernel",
57+
)
58+
@click.option(
59+
"-e",
60+
"--extra",
61+
type=str,
62+
help="Feature you'll be working on",
63+
)
64+
@click.argument("kernel", required=True, type=str, shell_complete=ShellCompletion.show_kernels)
65+
def checkout(kernel, change_dir, override, cleanup, extra):
66+
main(
67+
name=kernel,
68+
change_dir=change_dir,
69+
override=override,
70+
cleanup=cleanup,
71+
extra=extra,
72+
)

kt/commands/checkout/impl.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import logging
2+
import os
3+
import shutil
4+
5+
from kt.ktlib.config import Config
6+
from kt.ktlib.kernel_workspace import KernelWorkspace
7+
from kt.ktlib.kernels import KernelsInfo
8+
9+
10+
def main(name: str, change_dir: bool, cleanup: bool, override: bool, extra: str):
11+
config = Config.load()
12+
kernels = KernelsInfo.from_yaml(config=config).kernels
13+
if name not in kernels:
14+
raise ValueError(f"Invalid param: {name} does not exist")
15+
16+
kernel_info = kernels[name]
17+
kernel_workspace = KernelWorkspace.load(name=name, config=config, kernel_info=kernel_info, extra=extra)
18+
if cleanup:
19+
kernel_workspace.cleanup()
20+
return
21+
22+
if override:
23+
kernel_workspace.cleanup()
24+
25+
kernel_workspace.setup()
26+
if change_dir:
27+
shell_exec = os.environ["SHELL"]
28+
if not shutil.which(shell_exec):
29+
raise Exception(f"No executable {shell_exec}; cannot start a new one")
30+
31+
logging.info(f"Changing directory to {kernel_workspace.folder}")
32+
os.chdir(kernel_workspace.folder)
33+
os.execve(shell_exec, [shell_exec], os.environ)

kt/ktlib/kernel_workspace.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import logging
2+
import os
3+
from dataclasses import dataclass
4+
5+
from git import GitCommandError, Repo
6+
from pathlib3x import Path
7+
8+
from kt.ktlib.config import Config
9+
from kt.ktlib.kernels import KernelInfo
10+
from kt.ktlib.util import Constants
11+
12+
13+
@dataclass
14+
class RepoWorktree:
15+
source_root: Repo
16+
folder: Path
17+
remote: str
18+
remote_branch: str
19+
local_branch: str
20+
21+
@classmethod
22+
def load_from_filepath(cls, folder: Path):
23+
if not folder.exists():
24+
raise RuntimeError(f"{folder} does not exist")
25+
26+
# check if folder is a git repo
27+
repo = Repo(folder)
28+
29+
source_root = Path(repo.git.rev_parse("--path-format=absolute", "--git-common-dir")).parent
30+
remote = repo.remotes.origin
31+
32+
remote_branch = repo.active_branch.tracking_branch()
33+
34+
local_branch = repo.active_branch.name
35+
36+
return cls(
37+
source_root=source_root,
38+
folder=folder,
39+
remote=remote,
40+
remote_branch=remote_branch,
41+
local_branch=local_branch,
42+
)
43+
44+
def setup(self):
45+
"""
46+
First run: It will create the worktree
47+
Second run: It will update the worktree
48+
"""
49+
50+
try:
51+
remote_ref = f"{self.remote}/{self.remote_branch}"
52+
self.source_root.git.worktree(
53+
"add",
54+
"--track",
55+
"-b",
56+
self.local_branch,
57+
self.folder,
58+
remote_ref,
59+
)
60+
61+
except GitCommandError as e:
62+
if "already exists" in e.stderr:
63+
self.update()
64+
else:
65+
# Make sure the worktree is properly cleaned up
66+
self.cleanup()
67+
raise e
68+
69+
def update(self):
70+
"""
71+
It will make sure the worktree is up-to-date with remote.
72+
It assumes the worktree is already created and initialized.
73+
"""
74+
logging.info("update")
75+
repo = Repo(self.folder)
76+
77+
repo.remotes.origin.pull(rebase=True)
78+
79+
def cleanup(self):
80+
# remove worktree, only if it exists
81+
try:
82+
self.source_root.git.worktree("remove", self.folder, "-f")
83+
except GitCommandError as e:
84+
if f"'{self.folder}' is not a working tree" not in e.stderr:
85+
raise e
86+
87+
# remove local branch, only if it exists
88+
try:
89+
self.source_root.delete_head(self.local_branch, force=True)
90+
except GitCommandError as e:
91+
if f"branch '{self.local_branch}' not found" not in e.stderr:
92+
raise e
93+
94+
def push(self, force: bool = False):
95+
repo = Repo(self.folder)
96+
origin = repo.remote(name=self.remote)
97+
args = ["--set-upstream", origin.name, self.local_branch]
98+
if force:
99+
args.append("--force-with-lease")
100+
101+
repo.git.push(*args)
102+
103+
104+
@dataclass
105+
class KernelWorkspace:
106+
folder: Path
107+
dist_worktree: RepoWorktree
108+
src_worktree: RepoWorktree
109+
110+
@classmethod
111+
def load_from_filepath(cls, folder: Path):
112+
if not folder.exists():
113+
raise RuntimeError(f"{folder} does not exists")
114+
115+
## Get the dist-git-tree path
116+
dist_worktree_path = folder / Constants.DIST_TREE
117+
dist_worktree = RepoWorktree.load_from_filepath(folder=dist_worktree_path)
118+
119+
src_worktree_path = folder / Constants.SRC_TREE
120+
src_worktree = RepoWorktree.load_from_filepath(folder=src_worktree_path)
121+
122+
return cls(
123+
folder=folder,
124+
dist_worktree=dist_worktree,
125+
src_worktree=src_worktree,
126+
)
127+
128+
@classmethod
129+
def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str):
130+
if extra:
131+
name = name + "_" + extra
132+
133+
folder = config.kernels_dir / Path(name)
134+
user = os.environ["USER"]
135+
default_remote = "origin"
136+
137+
dist_folder = folder / Path(Constants.DIST_TREE)
138+
dist_local_branch = f"{{{user}}}_{kernel_info.dist_git_branch}"
139+
if extra:
140+
dist_local_branch += f"_{extra}"
141+
142+
dist_worktree = RepoWorktree(
143+
source_root=Repo(kernel_info.dist_git_root.folder),
144+
folder=dist_folder,
145+
remote=default_remote,
146+
remote_branch=kernel_info.dist_git_branch,
147+
local_branch=dist_local_branch,
148+
)
149+
150+
src_folder = folder / Path(Constants.SRC_TREE)
151+
src_local_branch = f"{{{user}}}_{kernel_info.src_tree_branch}"
152+
if extra:
153+
src_local_branch += f"_{extra}"
154+
155+
src_worktree = RepoWorktree(
156+
source_root=Repo(kernel_info.src_tree_root.folder),
157+
folder=src_folder,
158+
remote=default_remote,
159+
remote_branch=kernel_info.src_tree_branch,
160+
local_branch=src_local_branch,
161+
)
162+
163+
return cls(
164+
folder=folder,
165+
dist_worktree=dist_worktree,
166+
src_worktree=src_worktree,
167+
)
168+
169+
def setup(self):
170+
# Make sure the folder is created
171+
self.folder.mkdir(parents=True, exist_ok=True)
172+
173+
self.dist_worktree.setup()
174+
self.src_worktree.setup()
175+
176+
def cleanup(self):
177+
self.dist_worktree.cleanup()
178+
self.src_worktree.cleanup()
179+
180+
# Remove working directory that includes the above git worktrees
181+
self.folder.rmtree(ignore_errors=True)

kt/ktlib/shell_completion.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from kt.ktlib.config import Config
2+
from kt.ktlib.kernels import KernelsInfo
3+
4+
5+
class ShellCompletion:
6+
@classmethod
7+
def show_kernels(cls, ctx, param, incomplete):
8+
config = Config.load()
9+
kernels = KernelsInfo.from_yaml(config=config).kernels
10+
11+
return [kernel for kernel in kernels if kernel.startswith(incomplete)]

kt/ktlib/util.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
class Constants:
22
PRIVATE_REPOS_CONFIG_FILE = ".private_repos.yaml"
3+
SRC_TREE = "kernel-src-tree"
4+
DIST_TREE = "kernel-dist-git"
35
DIST_GIT_ROOT = "dist_git_root"
46
SRC_TREE_ROOT = "src_tree_root"
57
COMMON_REPOS = "common_repos"

0 commit comments

Comments
 (0)