Mention smart-make in a comment.
[rsync.git] / packaging / patch-update
blobfd56a9d8c72838c13d6f767d82edbb5b886e69b7
1 #!/usr/bin/env -S python3 -B
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
7 # diffs.
9 import os, sys, re, argparse, time, shutil
11 sys.path = ['packaging'] + sys.path
13 from pkglib import *
15 MAKE_GEN_CMDS = [
16         './prepare-source'.split(),
17         'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi',
18         'make -C build gen'.split(),
19         ]
20 TMP_DIR = "patches.gen"
22 os.environ['GIT_MERGE_AUTOEDIT'] = 'no'
24 def main():
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)
36     if cmd_txt_chk(['packaging/prep-auto-dir']).out == '':
37         die('You must setup an auto-build-save dir to use this script.')
39     if args.gen:
40         if os.path.lexists(TMP_DIR):
41             die(f'"{TMP_DIR}" must not exist in the current directory.')
42         gen_files = get_gen_files()
43         os.mkdir(TMP_DIR, 0o700)
44         for cmd in MAKE_GEN_CMDS:
45             cmd_chk(cmd)
46         cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/'])
48     last_touch = int(time.time())
50     # Start by finding all patches so that we can load all possible parents.
51     patches = sorted(list(get_patch_branches(args.base_branch)))
53     parent_patch = { }
54     description = { }
56     for patch in patches:
57         branch = f"patch/{args.base_branch}/{patch}"
58         desc = ''
59         proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"])
60         in_diff = False
61         for line in proc.stdout:
62             if in_diff:
63                 if not re.match(r'^[ +]', line):
64                     continue
65                 line = line[1:]
66                 m = re.search(r'patch -p1 <patches/(\S+)\.diff', line)
67                 if m and m[1] != patch:
68                     parpat = parent_patch[patch] = m[1]
69                     if not parpat in patches:
70                         die(f"Parent of {patch} is not a local branch: {parpat}")
71                 desc += line
72             elif re.match(r'^@@ ', line):
73                 in_diff = True
74         description[patch] = desc
75         proc.communicate()
77     if args.patch_files: # Limit the list of patches to actually process
78         valid_patches = patches
79         patches = [ ]
80         for fn in args.patch_files:
81             name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
82             if name not in valid_patches:
83                 die(f"Local branch not available for patch: {name}")
84             patches.append(name)
86     completed = set()
88     for patch in patches:
89         if patch in completed:
90             continue
91         if not update_patch(patch):
92             break
94     if args.gen:
95         shutil.rmtree(TMP_DIR)
97     while last_touch >= int(time.time()):
98         time.sleep(1)
99     cmd_chk(['git', 'checkout', starting_branch])
100     cmd_chk(['packaging/prep-auto-dir'], discard='output')
103 def update_patch(patch):
104     global last_touch
106     completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops.
108     parent = parent_patch.get(patch, None)
109     if parent:
110         if parent not in completed:
111             if not update_patch(parent):
112                 return 0
113         based_on = parent = f"patch/{args.base_branch}/{parent}"
114     else:
115         parent = args.base_branch
116         based_on = master_commit
118     print(f"======== {patch} ========")
120     while args.gen and last_touch >= int(time.time()):
121         time.sleep(1)
123     branch = f"patch/{args.base_branch}/{patch}"
124     s = cmd_run(['git', 'checkout', branch])
125     if s.returncode != 0:
126         return 0
128     s = cmd_run(['git', 'merge', based_on])
129     ok = s.returncode == 0
130     skip_shell = False
131     if not ok or args.cmd or args.make or args.shell:
132         cmd_chk(['packaging/prep-auto-dir'], discard='output')
133     if not ok:
134         print(f'"git merge {based_on}" incomplete -- please fix.')
135         if not run_a_shell(parent, patch):
136             return 0
137         if not args.make and not args.cmd:
138             skip_shell = True
139     if args.make:
140         if cmd_run(['packaging/smart-make']).returncode != 0:
141             if not run_a_shell(parent, patch):
142                 return 0
143             if not args.cmd:
144                 skip_shell = True
145     if args.cmd:
146         if cmd_run(args.cmd).returncode != 0:
147             if not run_a_shell(parent, patch):
148                 return 0
149             skip_shell = True
150     if args.shell and not skip_shell:
151         if not run_a_shell(parent, patch):
152             return 0
154     with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh:
155         fh.write(description[patch])
156         fh.write(f"\nbased-on: {based_on}\n")
158         if args.gen:
159             gen_files = get_gen_files()
160             for cmd in MAKE_GEN_CMDS:
161                 cmd_chk(cmd)
162             cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"])
163         else:
164             gen_files = [ ]
165         last_touch = int(time.time())
167         proc = cmd_pipe(['git', 'diff', based_on])
168         skipping = False
169         for line in proc.stdout:
170             if skipping:
171                 if not re.match(r'^diff --git a/', line):
172                     continue
173                 skipping = False
174             elif re.match(r'^diff --git a/PATCH', line):
175                 skipping = True
176                 continue
177             if not re.match(r'^index ', line):
178                 fh.write(line)
179         proc.communicate()
181         if args.gen:
182             e_tmp_dir = re.escape(TMP_DIR)
183             diff_re  = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir))
184             minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
185             plus_re  = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
187             if parent == args.base_branch:
188                 parent_dir = 'master'
189             else:
190                 m = re.search(r'([^/]+)$', parent)
191                 parent_dir = m[1]
193             proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"])
194             for line in proc.stdout:
195                 line = diff_re.sub(r'\1 a/\2 b/\3', line)
196                 line = minus_re.sub(r'--- a/\1', line)
197                 line =  plus_re.sub(r'+++ b/\1', line)
198                 fh.write(line)
199             proc.communicate()
201     return 1
204 def run_a_shell(parent, patch):
205     m = re.search(r'([^/]+)$', parent)
206     parent_dir = m[1]
207     os.environ['PS1'] = f"[{parent_dir}] {patch}: "
209     while True:
210         s = cmd_run([os.environ.get('SHELL', '/bin/sh')])
211         if s.returncode != 0:
212             ans = input("Abort? [n/y] ")
213             if re.match(r'^y', ans, flags=re.I):
214                 return False
215             continue
216         cur_branch, is_clean, status_txt = check_git_status(0)
217         if is_clean:
218             break
219         print(status_txt, end='')
221     cmd_run('rm -f build/*.o build/*/*.o')
223     return True
226 if __name__ == '__main__':
227     parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False)
228     parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.")
229     parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
230     parser.add_argument('--make', '-m', action='store_true', help="Run the smart-make script in every patch branch.")
231     parser.add_argument('--cmd', '-c', help="Run a command in every patch branch.")
232     parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.")
233     parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.')
234     parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.")
235     parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.")
236     parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
237     args = parser.parse_args()
238     if args.gen == '':
239         args.gen = args.patches_dir
240     elif args.gen is not None:
241         args.patches_dir = args.gen
242     main()
244 # vim: sw=4 et ft=python