1*b1e83836Smrg#!/usr/bin/env python3 2*b1e83836Smrg 3*b1e83836Smrg# Copyright (C) 2017-2019 Free Software Foundation, Inc. 4*b1e83836Smrg# 5*b1e83836Smrg# This file is part of GCC. 6*b1e83836Smrg# 7*b1e83836Smrg# GCC is free software; you can redistribute it and/or modify 8*b1e83836Smrg# it under the terms of the GNU General Public License as published by 9*b1e83836Smrg# the Free Software Foundation; either version 3, or (at your option) 10*b1e83836Smrg# any later version. 11*b1e83836Smrg# 12*b1e83836Smrg# GCC is distributed in the hope that it will be useful, 13*b1e83836Smrg# but WITHOUT ANY WARRANTY; without even the implied warranty of 14*b1e83836Smrg# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15*b1e83836Smrg# GNU General Public License for more details. 16*b1e83836Smrg# 17*b1e83836Smrg# You should have received a copy of the GNU General Public License 18*b1e83836Smrg# along with GCC; see the file COPYING. If not, write to 19*b1e83836Smrg# the Free Software Foundation, 51 Franklin Street, Fifth Floor, 20*b1e83836Smrg# Boston, MA 02110-1301, USA. 21*b1e83836Smrg 22*b1e83836Smrg# This script parses a .diff file generated with 'diff -up' or 'diff -cp' 23*b1e83836Smrg# and adds a skeleton ChangeLog file to the file. It does not try to be 24*b1e83836Smrg# too smart when parsing function names, but it produces a reasonable 25*b1e83836Smrg# approximation. 26*b1e83836Smrg# 27*b1e83836Smrg# This is a straightforward adaptation of original Perl script. 28*b1e83836Smrg# 29*b1e83836Smrg# Author: Yury Gribov <tetra2005@gmail.com> 30*b1e83836Smrg 31*b1e83836Smrgimport argparse 32*b1e83836Smrgimport sys 33*b1e83836Smrgimport re 34*b1e83836Smrgimport os.path 35*b1e83836Smrgimport os 36*b1e83836Smrgimport tempfile 37*b1e83836Smrgimport time 38*b1e83836Smrgimport shutil 39*b1e83836Smrgfrom subprocess import Popen, PIPE 40*b1e83836Smrg 41*b1e83836Smrgme = os.path.basename(sys.argv[0]) 42*b1e83836Smrg 43*b1e83836Smrgpr_regex = re.compile('\+(\/(\/|\*)|[Cc*!])\s+(PR [a-z+-]+\/[0-9]+)') 44*b1e83836Smrg 45*b1e83836Smrgdef error(msg): 46*b1e83836Smrg sys.stderr.write("%s: error: %s\n" % (me, msg)) 47*b1e83836Smrg sys.exit(1) 48*b1e83836Smrg 49*b1e83836Smrgdef warn(msg): 50*b1e83836Smrg sys.stderr.write("%s: warning: %s\n" % (me, msg)) 51*b1e83836Smrg 52*b1e83836Smrgclass RegexCache(object): 53*b1e83836Smrg """Simple trick to Perl-like combined match-and-bind.""" 54*b1e83836Smrg 55*b1e83836Smrg def __init__(self): 56*b1e83836Smrg self.last_match = None 57*b1e83836Smrg 58*b1e83836Smrg def match(self, p, s): 59*b1e83836Smrg self.last_match = re.match(p, s) if isinstance(p, str) else p.match(s) 60*b1e83836Smrg return self.last_match 61*b1e83836Smrg 62*b1e83836Smrg def search(self, p, s): 63*b1e83836Smrg self.last_match = re.search(p, s) if isinstance(p, str) else p.search(s) 64*b1e83836Smrg return self.last_match 65*b1e83836Smrg 66*b1e83836Smrg def group(self, n): 67*b1e83836Smrg return self.last_match.group(n) 68*b1e83836Smrg 69*b1e83836Smrgcache = RegexCache() 70*b1e83836Smrg 71*b1e83836Smrgdef run(cmd, die_on_error): 72*b1e83836Smrg """Simple wrapper for Popen.""" 73*b1e83836Smrg proc = Popen(cmd.split(' '), stderr = PIPE, stdout = PIPE) 74*b1e83836Smrg (out, err) = proc.communicate() 75*b1e83836Smrg if die_on_error and proc.returncode != 0: 76*b1e83836Smrg error("`%s` failed:\n" % (cmd, proc.stderr)) 77*b1e83836Smrg return proc.returncode, out.decode(), err 78*b1e83836Smrg 79*b1e83836Smrgdef read_user_info(): 80*b1e83836Smrg dot_mklog_format_msg = """\ 81*b1e83836SmrgThe .mklog format is: 82*b1e83836SmrgNAME = ... 83*b1e83836SmrgEMAIL = ... 84*b1e83836Smrg""" 85*b1e83836Smrg 86*b1e83836Smrg # First try to read .mklog config 87*b1e83836Smrg mklog_conf = os.path.expanduser('~/.mklog') 88*b1e83836Smrg if os.path.exists(mklog_conf): 89*b1e83836Smrg attrs = {} 90*b1e83836Smrg f = open(mklog_conf) 91*b1e83836Smrg for s in f: 92*b1e83836Smrg if cache.match(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*$', s): 93*b1e83836Smrg attrs[cache.group(1)] = cache.group(2) 94*b1e83836Smrg f.close() 95*b1e83836Smrg if 'NAME' not in attrs: 96*b1e83836Smrg error("'NAME' not present in .mklog") 97*b1e83836Smrg if 'EMAIL' not in attrs: 98*b1e83836Smrg error("'EMAIL' not present in .mklog") 99*b1e83836Smrg return attrs['NAME'], attrs['EMAIL'] 100*b1e83836Smrg 101*b1e83836Smrg # Otherwise go with git 102*b1e83836Smrg 103*b1e83836Smrg rc1, name, _ = run('git config user.name', False) 104*b1e83836Smrg name = name.rstrip() 105*b1e83836Smrg rc2, email, _ = run('git config user.email', False) 106*b1e83836Smrg email = email.rstrip() 107*b1e83836Smrg 108*b1e83836Smrg if rc1 != 0 or rc2 != 0: 109*b1e83836Smrg error("""\ 110*b1e83836SmrgCould not read git user.name and user.email settings. 111*b1e83836SmrgPlease add missing git settings, or create a %s. 112*b1e83836Smrg""" % mklog_conf) 113*b1e83836Smrg 114*b1e83836Smrg return name, email 115*b1e83836Smrg 116*b1e83836Smrgdef get_parent_changelog (s): 117*b1e83836Smrg """See which ChangeLog this file change should go to.""" 118*b1e83836Smrg 119*b1e83836Smrg if s.find('\\') == -1 and s.find('/') == -1: 120*b1e83836Smrg return "ChangeLog", s 121*b1e83836Smrg 122*b1e83836Smrg gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 123*b1e83836Smrg 124*b1e83836Smrg d = s 125*b1e83836Smrg while d: 126*b1e83836Smrg clname = d + "/ChangeLog" 127*b1e83836Smrg if os.path.exists(gcc_root + '/' + clname) or os.path.exists(clname): 128*b1e83836Smrg relname = s[len(d)+1:] 129*b1e83836Smrg return clname, relname 130*b1e83836Smrg d, _ = os.path.split(d) 131*b1e83836Smrg 132*b1e83836Smrg return "Unknown ChangeLog", s 133*b1e83836Smrg 134*b1e83836Smrgclass FileDiff: 135*b1e83836Smrg """Class to represent changes in a single file.""" 136*b1e83836Smrg 137*b1e83836Smrg def __init__(self, filename): 138*b1e83836Smrg self.filename = filename 139*b1e83836Smrg self.hunks = [] 140*b1e83836Smrg self.clname, self.relname = get_parent_changelog(filename); 141*b1e83836Smrg 142*b1e83836Smrg def dump(self): 143*b1e83836Smrg print("Diff for %s:\n ChangeLog = %s\n rel name = %s\n" % (self.filename, self.clname, self.relname)) 144*b1e83836Smrg for i, h in enumerate(self.hunks): 145*b1e83836Smrg print("Next hunk %d:" % i) 146*b1e83836Smrg h.dump() 147*b1e83836Smrg 148*b1e83836Smrgclass Hunk: 149*b1e83836Smrg """Class to represent a single hunk of changes.""" 150*b1e83836Smrg 151*b1e83836Smrg def __init__(self, hdr): 152*b1e83836Smrg self.hdr = hdr 153*b1e83836Smrg self.lines = [] 154*b1e83836Smrg self.ctx_diff = is_ctx_hunk_start(hdr) 155*b1e83836Smrg 156*b1e83836Smrg def dump(self): 157*b1e83836Smrg print('%s' % self.hdr) 158*b1e83836Smrg print('%s' % '\n'.join(self.lines)) 159*b1e83836Smrg 160*b1e83836Smrg def is_file_addition(self): 161*b1e83836Smrg """Does hunk describe addition of file?""" 162*b1e83836Smrg if self.ctx_diff: 163*b1e83836Smrg for line in self.lines: 164*b1e83836Smrg if re.match(r'^\*\*\* 0 \*\*\*\*', line): 165*b1e83836Smrg return True 166*b1e83836Smrg else: 167*b1e83836Smrg return re.match(r'^@@ -0,0 \+1.* @@', self.hdr) 168*b1e83836Smrg 169*b1e83836Smrg def is_file_removal(self): 170*b1e83836Smrg """Does hunk describe removal of file?""" 171*b1e83836Smrg if self.ctx_diff: 172*b1e83836Smrg for line in self.lines: 173*b1e83836Smrg if re.match(r'^--- 0 ----', line): 174*b1e83836Smrg return True 175*b1e83836Smrg else: 176*b1e83836Smrg return re.match(r'^@@ -1.* \+0,0 @@', self.hdr) 177*b1e83836Smrg 178*b1e83836Smrgdef is_file_diff_start(s): 179*b1e83836Smrg # Don't be fooled by context diff line markers: 180*b1e83836Smrg # *** 385,391 **** 181*b1e83836Smrg return ((s.startswith('*** ') and not s.endswith('***')) 182*b1e83836Smrg or (s.startswith('--- ') and not s.endswith('---'))) 183*b1e83836Smrg 184*b1e83836Smrgdef is_ctx_hunk_start(s): 185*b1e83836Smrg return re.match(r'^\*\*\*\*\*\**', s) 186*b1e83836Smrg 187*b1e83836Smrgdef is_uni_hunk_start(s): 188*b1e83836Smrg return re.match(r'^@@ .* @@', s) 189*b1e83836Smrg 190*b1e83836Smrgdef is_hunk_start(s): 191*b1e83836Smrg return is_ctx_hunk_start(s) or is_uni_hunk_start(s) 192*b1e83836Smrg 193*b1e83836Smrgdef remove_suffixes(s): 194*b1e83836Smrg if s.startswith('a/') or s.startswith('b/'): 195*b1e83836Smrg s = s[2:] 196*b1e83836Smrg if s.endswith('.jj'): 197*b1e83836Smrg s = s[:-3] 198*b1e83836Smrg return s 199*b1e83836Smrg 200*b1e83836Smrgdef find_changed_funs(hunk): 201*b1e83836Smrg """Find all functions touched by hunk. We don't try too hard 202*b1e83836Smrg to find good matches. This should return a superset 203*b1e83836Smrg of the actual set of functions in the .diff file. 204*b1e83836Smrg """ 205*b1e83836Smrg 206*b1e83836Smrg fns = [] 207*b1e83836Smrg fn = None 208*b1e83836Smrg 209*b1e83836Smrg if (cache.match(r'^\*\*\*\*\*\** ([a-zA-Z0-9_].*)', hunk.hdr) 210*b1e83836Smrg or cache.match(r'^@@ .* @@ ([a-zA-Z0-9_].*)', hunk.hdr)): 211*b1e83836Smrg fn = cache.group(1) 212*b1e83836Smrg 213*b1e83836Smrg for i, line in enumerate(hunk.lines): 214*b1e83836Smrg # Context diffs have extra whitespace after first char; 215*b1e83836Smrg # remove it to make matching easier. 216*b1e83836Smrg if hunk.ctx_diff: 217*b1e83836Smrg line = re.sub(r'^([-+! ]) ', r'\1', line) 218*b1e83836Smrg 219*b1e83836Smrg # Remember most recent identifier in hunk 220*b1e83836Smrg # that might be a function name. 221*b1e83836Smrg if cache.match(r'^[-+! ]([a-zA-Z0-9_#].*)', line): 222*b1e83836Smrg fn = cache.group(1) 223*b1e83836Smrg 224*b1e83836Smrg change = line and re.match(r'^[-+!][^-]', line) 225*b1e83836Smrg 226*b1e83836Smrg # Top-level comment cannot belong to function 227*b1e83836Smrg if re.match(r'^[-+! ]\/\*', line): 228*b1e83836Smrg fn = None 229*b1e83836Smrg 230*b1e83836Smrg if change and fn: 231*b1e83836Smrg if cache.match(r'^((class|struct|union|enum)\s+[a-zA-Z0-9_]+)', fn): 232*b1e83836Smrg # Struct declaration 233*b1e83836Smrg fn = cache.group(1) 234*b1e83836Smrg elif cache.search(r'#\s*define\s+([a-zA-Z0-9_]+)', fn): 235*b1e83836Smrg # Macro definition 236*b1e83836Smrg fn = cache.group(1) 237*b1e83836Smrg elif cache.match('^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)', fn): 238*b1e83836Smrg # Supermacro 239*b1e83836Smrg fn = cache.group(1) 240*b1e83836Smrg elif cache.search(r'([a-zA-Z_][^()\s]*)\s*\([^*]', fn): 241*b1e83836Smrg # Discard template and function parameters. 242*b1e83836Smrg fn = cache.group(1) 243*b1e83836Smrg fn = re.sub(r'<[^<>]*>', '', fn) 244*b1e83836Smrg fn = fn.rstrip() 245*b1e83836Smrg else: 246*b1e83836Smrg fn = None 247*b1e83836Smrg 248*b1e83836Smrg if fn and fn not in fns: # Avoid dups 249*b1e83836Smrg fns.append(fn) 250*b1e83836Smrg 251*b1e83836Smrg fn = None 252*b1e83836Smrg 253*b1e83836Smrg return fns 254*b1e83836Smrg 255*b1e83836Smrgdef parse_patch(contents): 256*b1e83836Smrg """Parse patch contents to a sequence of FileDiffs.""" 257*b1e83836Smrg 258*b1e83836Smrg diffs = [] 259*b1e83836Smrg 260*b1e83836Smrg lines = contents.split('\n') 261*b1e83836Smrg 262*b1e83836Smrg i = 0 263*b1e83836Smrg while i < len(lines): 264*b1e83836Smrg line = lines[i] 265*b1e83836Smrg 266*b1e83836Smrg # Diff headers look like 267*b1e83836Smrg # --- a/gcc/tree.c 268*b1e83836Smrg # +++ b/gcc/tree.c 269*b1e83836Smrg # or 270*b1e83836Smrg # *** gcc/cfgexpand.c 2013-12-25 20:07:24.800350058 +0400 271*b1e83836Smrg # --- gcc/cfgexpand.c 2013-12-25 20:06:30.612350178 +0400 272*b1e83836Smrg 273*b1e83836Smrg if is_file_diff_start(line): 274*b1e83836Smrg left = re.split(r'\s+', line)[1] 275*b1e83836Smrg else: 276*b1e83836Smrg i += 1 277*b1e83836Smrg continue 278*b1e83836Smrg 279*b1e83836Smrg left = remove_suffixes(left); 280*b1e83836Smrg 281*b1e83836Smrg i += 1 282*b1e83836Smrg line = lines[i] 283*b1e83836Smrg 284*b1e83836Smrg if not cache.match(r'^[+-][+-][+-] +(\S+)', line): 285*b1e83836Smrg error("expected filename in line %d" % i) 286*b1e83836Smrg right = remove_suffixes(cache.group(1)); 287*b1e83836Smrg 288*b1e83836Smrg # Extract real file name from left and right names. 289*b1e83836Smrg filename = None 290*b1e83836Smrg if left == right: 291*b1e83836Smrg filename = left 292*b1e83836Smrg elif left == '/dev/null': 293*b1e83836Smrg filename = right; 294*b1e83836Smrg elif right == '/dev/null': 295*b1e83836Smrg filename = left; 296*b1e83836Smrg else: 297*b1e83836Smrg comps = [] 298*b1e83836Smrg while left and right: 299*b1e83836Smrg left, l = os.path.split(left) 300*b1e83836Smrg right, r = os.path.split(right) 301*b1e83836Smrg if l != r: 302*b1e83836Smrg break 303*b1e83836Smrg comps.append(l) 304*b1e83836Smrg 305*b1e83836Smrg if not comps: 306*b1e83836Smrg error("failed to extract common name for %s and %s" % (left, right)) 307*b1e83836Smrg 308*b1e83836Smrg comps.reverse() 309*b1e83836Smrg filename = '/'.join(comps) 310*b1e83836Smrg 311*b1e83836Smrg d = FileDiff(filename) 312*b1e83836Smrg diffs.append(d) 313*b1e83836Smrg 314*b1e83836Smrg # Collect hunks for current file. 315*b1e83836Smrg hunk = None 316*b1e83836Smrg i += 1 317*b1e83836Smrg while i < len(lines): 318*b1e83836Smrg line = lines[i] 319*b1e83836Smrg 320*b1e83836Smrg # Create new hunk when we see hunk header 321*b1e83836Smrg if is_hunk_start(line): 322*b1e83836Smrg if hunk is not None: 323*b1e83836Smrg d.hunks.append(hunk) 324*b1e83836Smrg hunk = Hunk(line) 325*b1e83836Smrg i += 1 326*b1e83836Smrg continue 327*b1e83836Smrg 328*b1e83836Smrg # Stop when we reach next diff 329*b1e83836Smrg if (is_file_diff_start(line) 330*b1e83836Smrg or line.startswith('diff ') 331*b1e83836Smrg or line.startswith('Index: ')): 332*b1e83836Smrg i -= 1 333*b1e83836Smrg break 334*b1e83836Smrg 335*b1e83836Smrg if hunk is not None: 336*b1e83836Smrg hunk.lines.append(line) 337*b1e83836Smrg i += 1 338*b1e83836Smrg 339*b1e83836Smrg d.hunks.append(hunk) 340*b1e83836Smrg 341*b1e83836Smrg return diffs 342*b1e83836Smrg 343*b1e83836Smrg 344*b1e83836Smrgdef get_pr_from_testcase(line): 345*b1e83836Smrg r = pr_regex.search(line) 346*b1e83836Smrg if r != None: 347*b1e83836Smrg return r.group(3) 348*b1e83836Smrg else: 349*b1e83836Smrg return None 350*b1e83836Smrg 351*b1e83836Smrgdef main(): 352*b1e83836Smrg name, email = read_user_info() 353*b1e83836Smrg 354*b1e83836Smrg help_message = """\ 355*b1e83836SmrgGenerate ChangeLog template for PATCH. 356*b1e83836SmrgPATCH must be generated using diff(1)'s -up or -cp options 357*b1e83836Smrg(or their equivalent in Subversion/git). 358*b1e83836Smrg""" 359*b1e83836Smrg 360*b1e83836Smrg inline_message = """\ 361*b1e83836SmrgPrepends ChangeLog to PATCH. 362*b1e83836SmrgIf PATCH is not stdin, modifies PATCH in-place, 363*b1e83836Smrgotherwise writes to stdout.' 364*b1e83836Smrg""" 365*b1e83836Smrg 366*b1e83836Smrg parser = argparse.ArgumentParser(description = help_message) 367*b1e83836Smrg parser.add_argument('-v', '--verbose', action = 'store_true', help = 'Verbose messages') 368*b1e83836Smrg parser.add_argument('-i', '--inline', action = 'store_true', help = inline_message) 369*b1e83836Smrg parser.add_argument('input', nargs = '?', help = 'Patch file (or missing, read standard input)') 370*b1e83836Smrg args = parser.parse_args() 371*b1e83836Smrg if args.input == '-': 372*b1e83836Smrg args.input = None 373*b1e83836Smrg input = open(args.input) if args.input else sys.stdin 374*b1e83836Smrg contents = input.read() 375*b1e83836Smrg diffs = parse_patch(contents) 376*b1e83836Smrg 377*b1e83836Smrg if args.verbose: 378*b1e83836Smrg print("Parse results:") 379*b1e83836Smrg for d in diffs: 380*b1e83836Smrg d.dump() 381*b1e83836Smrg 382*b1e83836Smrg # Generate template ChangeLog. 383*b1e83836Smrg 384*b1e83836Smrg logs = {} 385*b1e83836Smrg prs = [] 386*b1e83836Smrg for d in diffs: 387*b1e83836Smrg log_name = d.clname 388*b1e83836Smrg 389*b1e83836Smrg logs.setdefault(log_name, '') 390*b1e83836Smrg logs[log_name] += '\t* %s' % d.relname 391*b1e83836Smrg 392*b1e83836Smrg change_msg = '' 393*b1e83836Smrg 394*b1e83836Smrg # Check if file was removed or added. 395*b1e83836Smrg # Two patterns for context and unified diff. 396*b1e83836Smrg if len(d.hunks) == 1: 397*b1e83836Smrg hunk0 = d.hunks[0] 398*b1e83836Smrg if hunk0.is_file_addition(): 399*b1e83836Smrg if re.search(r'testsuite.*(?<!\.exp)$', d.filename): 400*b1e83836Smrg change_msg = ': New test.\n' 401*b1e83836Smrg pr = get_pr_from_testcase(hunk0.lines[0]) 402*b1e83836Smrg if pr and pr not in prs: 403*b1e83836Smrg prs.append(pr) 404*b1e83836Smrg else: 405*b1e83836Smrg change_msg = ": New file.\n" 406*b1e83836Smrg elif hunk0.is_file_removal(): 407*b1e83836Smrg change_msg = ": Remove.\n" 408*b1e83836Smrg 409*b1e83836Smrg _, ext = os.path.splitext(d.filename) 410*b1e83836Smrg if (not change_msg and ext in ['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def'] 411*b1e83836Smrg and not 'testsuite' in d.filename): 412*b1e83836Smrg fns = [] 413*b1e83836Smrg for hunk in d.hunks: 414*b1e83836Smrg for fn in find_changed_funs(hunk): 415*b1e83836Smrg if fn not in fns: 416*b1e83836Smrg fns.append(fn) 417*b1e83836Smrg 418*b1e83836Smrg for fn in fns: 419*b1e83836Smrg if change_msg: 420*b1e83836Smrg change_msg += "\t(%s):\n" % fn 421*b1e83836Smrg else: 422*b1e83836Smrg change_msg = " (%s):\n" % fn 423*b1e83836Smrg 424*b1e83836Smrg logs[log_name] += change_msg if change_msg else ":\n" 425*b1e83836Smrg 426*b1e83836Smrg if args.inline and args.input: 427*b1e83836Smrg # Get a temp filename, rather than an open filehandle, because we use 428*b1e83836Smrg # the open to truncate. 429*b1e83836Smrg fd, tmp = tempfile.mkstemp("tmp.XXXXXXXX") 430*b1e83836Smrg os.close(fd) 431*b1e83836Smrg 432*b1e83836Smrg # Copy permissions to temp file 433*b1e83836Smrg # (old Pythons do not support shutil.copymode) 434*b1e83836Smrg shutil.copymode(args.input, tmp) 435*b1e83836Smrg 436*b1e83836Smrg # Open the temp file, clearing contents. 437*b1e83836Smrg out = open(tmp, 'w') 438*b1e83836Smrg else: 439*b1e83836Smrg tmp = None 440*b1e83836Smrg out = sys.stdout 441*b1e83836Smrg 442*b1e83836Smrg # Print log 443*b1e83836Smrg date = time.strftime('%Y-%m-%d') 444*b1e83836Smrg bugmsg = '' 445*b1e83836Smrg if len(prs): 446*b1e83836Smrg bugmsg = '\n'.join(['\t' + pr for pr in prs]) + '\n' 447*b1e83836Smrg 448*b1e83836Smrg for log_name, msg in sorted(logs.items()): 449*b1e83836Smrg out.write("""\ 450*b1e83836Smrg%s: 451*b1e83836Smrg 452*b1e83836Smrg%s %s <%s> 453*b1e83836Smrg 454*b1e83836Smrg%s%s\n""" % (log_name, date, name, email, bugmsg, msg)) 455*b1e83836Smrg 456*b1e83836Smrg if args.inline: 457*b1e83836Smrg # Append patch body 458*b1e83836Smrg out.write(contents) 459*b1e83836Smrg 460*b1e83836Smrg if args.input: 461*b1e83836Smrg # Write new contents atomically 462*b1e83836Smrg out.close() 463*b1e83836Smrg shutil.move(tmp, args.input) 464*b1e83836Smrg 465*b1e83836Smrgif __name__ == '__main__': 466*b1e83836Smrg main() 467