3 # This script is used to turn one or more of the "patch/BASE/*" branches
4 # into one or more diffs in the "patches" directory. Pass the option
5 # --gen if you want generated files in the diffs. Pass the name of
6 # one or more diffs if you want to just update a subset of all the
9 import os, sys, re, argparse, time, shutil
11 sys.path = ['packaging'] + sys.path
16 'make -f prepare-source.mak conf'.split(),
17 './config.status'.split(),
20 TMP_DIR = "patches.gen"
22 os.environ['GIT_MERGE_AUTOEDIT'] = 'no'
25 global master_commit, parent_patch, description, completed, last_touch
27 if not os.path.isdir(args.patches_dir):
28 die(f'No "{args.patches_dir}" directory was found.')
29 if not os.path.isdir('.git'):
30 die('No ".git" directory present in the current dir.')
32 starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)
34 master_commit = latest_git_hash(args.base_branch)
37 if os.path.lexists(TMP_DIR):
38 die(f'"{TMP_DIR}" must not exist in the current directory.')
39 gen_files = get_gen_files()
40 os.mkdir(TMP_DIR, 0o700)
41 for cmd in MAKE_GEN_CMDS:
43 cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/'])
45 last_touch = time.time()
47 # Start by finding all patches so that we can load all possible parents.
48 patches = sorted(list(get_patch_branches(args.base_branch)))
54 branch = f"patch/{args.base_branch}/{patch}"
56 proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"])
58 for line in proc.stdout:
60 if not re.match(r'^[ +]', line):
63 m = re.search(r'patch -p1 <patches/(\S+)\.diff', line)
64 if m and m[1] != patch:
65 parpat = parent_patch[patch] = m[1]
66 if not parpat in patches:
67 die(f"Parent of {patch} is not a local branch: {parpat}")
69 elif re.match(r'^@@ ', line):
71 description[patch] = desc
74 if args.patch_files: # Limit the list of patches to actually process
75 valid_patches = patches
77 for fn in args.patch_files:
78 name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
79 if name not in valid_patches:
80 die(f"Local branch not available for patch: {name}")
86 if patch in completed:
88 if not update_patch(patch):
92 shutil.rmtree(TMP_DIR)
94 while last_touch >= time.time():
96 cmd_chk(['git', 'checkout', starting_branch])
99 def update_patch(patch):
102 completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops.
104 parent = parent_patch.get(patch, None)
106 if parent not in completed:
107 if not update_patch(parent):
109 based_on = parent = f"patch/{args.base_branch}/{parent}"
111 parent = args.base_branch
112 based_on = master_commit
114 print(f"======== {patch} ========")
116 while args.gen and last_touch >= time.time():
118 s = cmd_run(f"git checkout patch/{args.base_branch}/{patch}".split())
119 if s.returncode != 0:
122 s = cmd_run(['git', 'merge', based_on])
123 ok = s.returncode == 0
124 if not ok or args.shell:
125 m = re.search(r'([^/]+)$', parent)
128 print(f'"git merge {based_on}" incomplete -- please fix.')
129 os.environ['PS1'] = f"[{parent_dir}] {patch}: "
131 s = cmd_run([os.environ.get('SHELL', '/bin/sh')])
132 if s.returncode != 0:
133 ans = input("Abort? [n/y] ")
134 if re.match(r'^y', ans, flags=re.I):
137 cur_branch, is_clean, status_txt = check_git_status(0)
140 print(status_txt, end='')
142 with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh:
143 fh.write(description[patch])
144 fh.write(f"\nbased-on: {based_on}\n")
147 gen_files = get_gen_files()
148 for cmd in MAKE_GEN_CMDS:
150 cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"])
153 last_touch = time.time()
155 proc = cmd_pipe(['git', 'diff', based_on])
157 for line in proc.stdout:
159 if not re.match(r'^diff --git a/', line):
162 elif re.match(r'^diff --git a/PATCH', line):
165 if not re.match(r'^index ', line):
170 e_tmp_dir = re.escape(TMP_DIR)
171 diff_re = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir))
172 minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
173 plus_re = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
175 if parent == args.base_branch:
176 parent_dir = 'master'
178 m = re.search(r'([^/]+)$', parent)
181 proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"])
182 for line in proc.stdout:
183 line = diff_re.sub(r'\1 a/\2 b/\3', line)
184 line = minus_re.sub(r'--- a/\1', line)
185 line = plus_re.sub(r'+++ b/\1', line)
194 if __name__ == '__main__':
195 parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False)
196 parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.")
197 parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
198 parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.")
199 parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.')
200 parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.")
201 parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.")
202 parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
203 args = parser.parse_args()
205 args.gen = args.patches_dir
206 elif args.gen is not None:
207 args.patches_dir = args.gen