Coverage for bugscpp/processor/checkout.py : 94%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Checkout command.
4Clone a repository into the given directory on the host machine.
5"""
6import os.path
7import shutil
8import sys
9from pathlib import Path
10from textwrap import dedent
11from typing import Dict, List, Type
13import git
14import taxonomy
15from errors import (DppGitApplyPatchError, DppGitCheckoutError, DppGitCheckoutInvalidRepositoryError, DppGitCloneError,
16 DppGitError, DppGitPatchNotAppliedError, DppGitSubmoduleInitError, DppGitWorktreeError)
17from message import message
18from processor.core.argparser import create_common_vcs_parser
19from processor.core.command import Command
20from processor.core.data import Project
21from processor.core.docker import Docker
24def _git_clone(path: Path, metadata: taxonomy.MetaData) -> git.Repo:
25 """
26 Initialize repository or clone a new one if it does not exist.
27 """
28 try:
29 repo = git.Repo(str(path))
30 except git.NoSuchPathError:
31 if not path.parent.exists():
32 path.parent.mkdir(parents=True, exist_ok=True)
33 message.info(__name__, f"cloning {metadata.name} into {str(path)}")
34 try:
35 repo = git.Repo.clone_from(
36 metadata.info.url, str(path), multi_options=["-c core.autocrlf=false"]
37 )
38 except git.GitCommandError as e:
39 raise DppGitCloneError(metadata, str(path), e.command, e.status, e.stdout)
41 return repo
44def _git_checkout(
45 repo: git.Repo, checkout_dir: Path, defect: taxonomy.Defect
46) -> git.Repo:
47 """
48 Checkout branch to the given commit.
49 """
50 if not checkout_dir.exists():
51 try:
52 # Pass '-f' in case worktree directory could be registered but removed.
53 repo.git.worktree("add", "-f", str(checkout_dir.resolve()), defect.hash)
54 except git.GitCommandError:
55 raise DppGitWorktreeError(repo, str(checkout_dir.resolve()), defect)
57 # git worktree list --porcelain will output
58 # $ worktree path
59 # $ ...
60 porcelain_output = repo.git.worktree("list", "--porcelain").split("\n\n")
61 dir_start_index = len("worktree ") + 1
62 directory_names = [
63 Path(output.splitlines()[0][dir_start_index:]).name
64 for output in porcelain_output
65 ]
66 if checkout_dir.name not in directory_names:
67 # Not sure if this is reachable.
68 raise DppGitCheckoutError(repo, str(checkout_dir), defect)
70 try:
71 return git.Repo(checkout_dir)
72 except git.exc.InvalidGitRepositoryError:
73 raise DppGitCheckoutInvalidRepositoryError(repo, str(checkout_dir), defect)
76def _git_am(repo: git.Repo, patches: List[str]):
77 """
78 Apply patches to checkout branch.
79 """
80 # Invoke command manually, because it seems like GitPython has a bug with updating submodules.
81 if repo.submodules:
82 try:
83 repo.git.execute(["git", "submodule", "update", "--init"])
84 except git.exc.GitError as e:
85 raise DppGitSubmoduleInitError(repo, e.command, e.status, e.stderr)
87 prev_hash = repo.git.rev_parse("--verify", "HEAD")
88 patches = list(filter(None, patches))
89 if patches:
90 message.info(
91 __name__, f"{', '.join(os.path.basename(patch) for patch in patches)}"
92 )
93 else:
94 message.info(__name__, "no patches")
96 for patch in patches:
97 try:
98 repo.git.am(patch)
99 except git.GitCommandError as e:
100 raise DppGitApplyPatchError(repo, patch, e.command, e.status, e.stderr)
102 current_hash = repo.git.rev_parse("--verify", "HEAD")
103 if prev_hash == current_hash:
104 raise DppGitPatchNotAppliedError(repo, patch)
105 prev_hash = current_hash
108class CheckoutCommand(Command):
109 """
110 Checkout command which handles VCS commands based on taxonomy information.
111 """
113 _ERROR_MESSAGES: Dict[Type[DppGitError], str] = {
114 DppGitCloneError: "git-clone failed",
115 DppGitWorktreeError: "git-worktree failed",
116 DppGitCheckoutInvalidRepositoryError: "git-checkout failed (not a git repository)",
117 DppGitCheckoutError: "git-checkout failed",
118 DppGitSubmoduleInitError: "git-submodule failed",
119 DppGitApplyPatchError: "git-am failed",
120 DppGitPatchNotAppliedError: "git-am patch could not be applied",
121 }
123 def __init__(self):
124 super().__init__()
125 # TODO: write argparse description in detail
126 self.parser = create_common_vcs_parser()
127 self.parser.usage = "bugcpp.py checkout PROJECT INDEX [-b|--buggy] [-t|--target WORKSPACE] [-s|--source-only] [-v|--verbose]"
128 self.parser.description = dedent(
129 """\
130 Checkout defect taxonomy.
131 """
132 )
134 def __call__(self, argv: List[str]):
135 """
136 Clone a repository into the given directory or checkout to a specific commit on the host machine.
137 It does not perform action inside a container unlike the other commands.
138 It utilizes git-worktree rather than cleaning up the current directory and checking out.
139 It not only makes hoping around commits more quickly, but also reduces overhead of writing and deleting files.
141 Parameters
142 ----------
143 argv : List[str]
144 Command line argument vector.
146 Returns
147 -------
148 None
149 """
150 args = self.parser.parse_args(argv)
151 metadata = args.metadata
152 metadata_base = args.metadata_base
153 worktree = args.worktree
154 # args.index is 1 based.
155 defect = metadata.defects[args.index - 1]
157 try:
158 message.info(__name__, f"git-clone '{metadata.name}'")
159 message.stdout_progress(
160 f"[{metadata.name}] cloning a new repository from '{metadata.info.url}'"
161 )
162 repo = _git_clone(worktree.base / ".repo", metadata)
164 message.info(__name__, "git-checkout")
165 message.stdout_progress(f"[{metadata.name}] checking out '{defect.hash}'")
166 checkout_repo = _git_checkout(repo, worktree.host, defect)
168 current_hash = checkout_repo.git.rev_parse("--verify", "HEAD")
169 if current_hash == defect.hash:
170 message.info(__name__, "git-am")
171 _git_am(
172 checkout_repo,
173 [
174 defect.common_patch,
175 defect.split_patch,
176 defect.buggy_patch if args.buggy else defect.fixed_patch,
177 ],
178 )
179 else:
180 message.info(__name__, "git-am skipped")
182 # check if there are extra data
183 path_to_extra = Path(metadata_base).joinpath(
184 metadata.name, "extra", f"{args.index:04}"
185 )
186 if path_to_extra.exists():
187 message.info(
188 __name__,
189 f"copying extra directory(f{path_to_extra}) to project root",
190 )
191 for extra in Path.iterdir(path_to_extra):
192 shutil.copytree(
193 extra, Path(worktree.host, extra.stem), dirs_exist_ok=True
194 )
196 message.info(__name__, f"creating '.defects4cpp.json' at {worktree.host}")
197 # Write .defects4cpp.json in the directory.
198 Project.write_config(worktree)
199 if not args.source_only:
200 try:
201 docker = Docker(metadata.dockerfile, worktree, verbose=args.verbose)
202 image = docker.image
203 message.info(__name__, f" image: '{image}'")
204 except Exception as e:
205 message.stdout_error(
206 f" An API Error occured.{os.linesep}"
207 f" Find detailed message at {message.path}."
208 )
210 except DppGitError as e:
211 message.error(__name__, str(e))
212 message.stdout_progress_error(f"[{metadata.name}] {str(e)}")
213 sys.exit(1)
215 # pull docker image if it does not exist
216 message.info(__name__, "pulling docker image")
217 dockerfile = metadata.dockerfile
218 tag = Path(dockerfile).parent.name
219 self._container_name: str = f"{tag}-dpp"
220 self._tag = f"hschoe/defects4cpp-ubuntu:{tag}"
222 message.stdout_progress(f"[{metadata.name}] done")
224 @property
225 def group(self) -> str:
226 return "v1"
228 @property
229 def help(self) -> str:
230 return "Get a specific defect snapshot"