blob: ccf668b738f301aef6e89865411b1e2c07fa3fdc [file] [log] [blame]
Austin Schuh812d0d12021-11-04 20:16:48 -07001import os
2import re
3import shutil
4import subprocess
5import tempfile
6
7
James Kuszmaulcf324122023-01-14 14:07:17 -08008def clone_repo(url, treeish, shallow=True):
9 """Clones a Git repo at the given URL into a temp folder, checks out the
10 given tree-ish (either branch or tag), then returns the repo root.
Austin Schuh812d0d12021-11-04 20:16:48 -070011
12 Keyword argument:
James Kuszmaulcf324122023-01-14 14:07:17 -080013 url -- The URL of the Git repo
Austin Schuh812d0d12021-11-04 20:16:48 -070014 treeish -- The tree-ish to check out (branch or tag)
James Kuszmaulcf324122023-01-14 14:07:17 -080015 shallow -- Whether to do a shallow clone
16
17 Returns:
18 root -- root directory of the cloned Git repository
Austin Schuh812d0d12021-11-04 20:16:48 -070019 """
James Kuszmaulcf324122023-01-14 14:07:17 -080020 cwd = os.getcwd()
James Kuszmaulb13e13f2023-11-22 20:44:04 -080021 if url.startswith("file://"):
22 os.chdir(os.path.dirname(url[7:]))
23 else:
24 os.chdir(tempfile.gettempdir())
Austin Schuh812d0d12021-11-04 20:16:48 -070025
26 repo = os.path.basename(url)
James Kuszmaulcf324122023-01-14 14:07:17 -080027 dest = os.path.join(os.getcwd(), repo)
28 if dest.endswith(".git"):
29 dest = dest[:-4]
Austin Schuh812d0d12021-11-04 20:16:48 -070030
31 # Clone Git repository into current directory or update it
32 if not os.path.exists(dest):
James Kuszmaulcf324122023-01-14 14:07:17 -080033 cmd = ["git", "clone"]
34 if shallow:
35 cmd += ["--branch", treeish, "--depth", "1"]
36 subprocess.run(cmd + [url, dest])
Austin Schuh812d0d12021-11-04 20:16:48 -070037 os.chdir(dest)
38 else:
39 os.chdir(dest)
40 subprocess.run(["git", "fetch", "origin", treeish])
41
42 # Get list of heads
43 # Example output of "git ls-remote --heads":
44 # From https://gitlab.com/libeigen/eigen.git
45 # 77c66e368c7e355f8be299659f57b0ffcaedb505 refs/heads/3.4
46 # 3e006bfd31e4389e8c5718c30409cddb65a73b04 refs/heads/master
James Kuszmaulcf324122023-01-14 14:07:17 -080047 ls_out = subprocess.check_output(["git", "ls-remote", "--heads"]).decode().rstrip()
Austin Schuh812d0d12021-11-04 20:16:48 -070048 heads = [x.split()[1] for x in ls_out.split("\n")[1:]]
49
50 if f"refs/heads/{treeish}" in heads:
51 # Checking out the remote branch avoids needing to handle syncing a
52 # preexisting local one
53 subprocess.run(["git", "checkout", f"origin/{treeish}"])
54 else:
55 subprocess.run(["git", "checkout", treeish])
56
James Kuszmaulcf324122023-01-14 14:07:17 -080057 os.chdir(cwd)
58 return dest
59
Austin Schuh812d0d12021-11-04 20:16:48 -070060
61def get_repo_root():
62 """Returns the Git repository root as an absolute path.
63
64 An empty string is returned if no repository root was found.
65 """
66 current_dir = os.path.abspath(os.getcwd())
67 while current_dir != os.path.dirname(current_dir):
68 if os.path.exists(current_dir + os.sep + ".git"):
69 return current_dir
70 current_dir = os.path.dirname(current_dir)
71 return ""
72
73
Austin Schuh812d0d12021-11-04 20:16:48 -070074def walk_if(top, pred):
75 """Walks the current directory, then returns a list of files for which the
76 given predicate is true.
77
78 Keyword arguments:
79 top -- the top directory to walk
80 pred -- a function that takes a directory path and a filename, then returns
81 True if the file should be included in the output list
82 """
83 return [
James Kuszmaulcf324122023-01-14 14:07:17 -080084 os.path.join(dp, f) for dp, dn, fn in os.walk(top) for f in fn if pred(dp, f)
Austin Schuh812d0d12021-11-04 20:16:48 -070085 ]
86
87
88def copy_to(files, root):
89 """Copies list of files to root by appending the relative paths of the files
90 to root.
91
92 The leading directories of root will be created if they don't yet exist.
93
94 Keyword arguments:
95 files -- list of files to copy
96 root -- destination
97
98 Returns:
99 The list of files in their destination.
100 """
101 if not os.path.exists(root):
102 os.makedirs(root)
103
104 dest_files = []
105 for f in files:
106 dest_file = os.path.join(root, f)
107
108 # Rename .cc file to .cpp
109 if dest_file.endswith(".cc"):
110 dest_file = os.path.splitext(dest_file)[0] + ".cpp"
James Kuszmaulcf324122023-01-14 14:07:17 -0800111 if dest_file.endswith(".c"):
112 dest_file = os.path.splitext(dest_file)[0] + ".cpp"
Austin Schuh812d0d12021-11-04 20:16:48 -0700113
114 # Make leading directory
115 dest_dir = os.path.dirname(dest_file)
116 if not os.path.exists(dest_dir):
117 os.makedirs(dest_dir)
118
119 shutil.copyfile(f, dest_file)
120 dest_files.append(dest_file)
121 return dest_files
122
123
124def walk_cwd_and_copy_if(pred, root):
125 """Walks the current directory, generates a list of files for which the
126 given predicate is true, then copies that list to root by appending the
127 relative paths of the files to root.
128
129 The leading directories of root will be created if they don't yet exist.
130
131 Keyword arguments:
132 pred -- a function that takes a directory path and a filename, then returns
133 True if the file should be included in the output list
134 root -- destination
135
136 Returns:
137 The list of files in their destination.
138 """
139 files = walk_if(".", pred)
140 files = copy_to(files, root)
141 return files
142
143
144def comment_out_invalid_includes(filename, include_roots):
145 """Comment out #include directives that include a nonexistent file
146
147 Keyword arguments:
148 filename -- file to search for includes
149 include_roots -- list of search paths for includes
150 """
151 # Read header
152 with open(filename) as f:
153 old_contents = f.read()
154
155 new_contents = ""
156 pos = 0
157 for match in re.finditer(r"#include \"([^\"]+)\"", old_contents):
158 include = match.group(1)
159
160 # Write contents from before this match
James Kuszmaulcf324122023-01-14 14:07:17 -0800161 new_contents += old_contents[pos : match.span()[0]]
Austin Schuh812d0d12021-11-04 20:16:48 -0700162
163 # Comment out #include if the file doesn't exist in current directory or
164 # include root
James Kuszmaulcf324122023-01-14 14:07:17 -0800165 if not os.path.exists(
166 os.path.join(os.path.dirname(filename), include)
167 ) and not any(
168 os.path.exists(os.path.join(include_root, include))
169 for include_root in include_roots
170 ):
Austin Schuh812d0d12021-11-04 20:16:48 -0700171 new_contents += "// "
172
173 new_contents += match.group()
174 pos = match.span()[1]
175
176 # Write rest of file if it wasn't all processed
177 if pos < len(old_contents):
178 new_contents += old_contents[pos:]
179
180 # Write modified file back out
181 if old_contents != new_contents:
182 with open(filename, "w") as f:
183 f.write(new_contents)
184
185
James Kuszmaulcf324122023-01-14 14:07:17 -0800186def git_am(patch, use_threeway=False, ignore_whitespace=False):
187 """Apply patch to a Git repository in the current directory using "git am".
Austin Schuh812d0d12021-11-04 20:16:48 -0700188
189 Keyword arguments:
James Kuszmaulcf324122023-01-14 14:07:17 -0800190 patch -- patch file relative to the root
191 use_threeway -- use a three-way merge when applying the patch
192 ignore_whitespace -- ignore whitespace in the patch file
Austin Schuh812d0d12021-11-04 20:16:48 -0700193 """
James Kuszmaulcf324122023-01-14 14:07:17 -0800194 args = ["git", "am"]
195 if use_threeway:
196 args.append("-3")
197 if ignore_whitespace:
198 args.append("--ignore-whitespace")
199
200 subprocess.check_output(args + [patch])