/ backport.py
backport.py
1 #!/usr/bin/env python3 2 ''' 3 Script to do backports (pull ids listed in to_backport.txt or command line) in order of merge, 4 to minimize number of conflicts. 5 ''' 6 import git 7 import re 8 import shlex 9 import subprocess 10 import os, sys 11 12 # External tools (can be overridden using environment) 13 GIT = os.getenv('GIT','git') 14 BASH = os.getenv('BASH','bash') 15 # Other configuration 16 SRCREPO = os.getenv('SRCREPO', '../bitcoin') 17 BRANCH = os.getenv('BRANCH', 'master') 18 19 def ask_prompt(text): 20 print(text,end=" ",file=sys.stderr) 21 sys.stderr.flush() 22 reply = sys.stdin.readline().rstrip() 23 print("",file=sys.stderr) 24 return reply 25 26 merge_re = re.compile('^Merge (\w+(?:-\w+)*/\w+(?:-\w+)*#[0-9]+)') 27 if len(sys.argv) > 1: 28 pulls = [x.strip() for x in sys.argv[1:]] 29 else: 30 with open('to_backport.txt','r') as f: 31 pulls = [x.strip() for x in f if x.strip()] 32 33 execute = True 34 35 pulls = set(pulls) 36 repo = git.Repo(SRCREPO) 37 head = repo.heads[BRANCH] 38 39 commit = head.commit 40 to_backport = [] 41 while True: 42 match = merge_re.match(commit.message) 43 if match: 44 prid = match.group(1) 45 if prid in pulls: 46 pulls.remove(prid) 47 to_backport.append((prid, commit)) 48 if not pulls: 49 break 50 if not commit.parents: 51 break 52 commit = commit.parents[0] 53 54 if pulls: 55 print('Missing: %s' % list(pulls)) 56 exit(1) 57 58 # Backport in reverse order 59 to_backport.reverse() 60 61 if execute: 62 class Attr: 63 hsh = "\x1b[90m" 64 head = "\x1b[1;96m" 65 head2 = "\x1b[94m" 66 reset = "\x1b[0m" 67 else: # no colors if we're printing a bash script 68 class Attr: 69 head = "" 70 head2 = "" 71 reset = "" 72 73 if not execute: 74 print('set -e') 75 for t in to_backport: 76 msg = t[1].message.rstrip().splitlines() 77 assert(msg[1] == '') 78 print('{a.hsh}# {a.head}{}{a.reset}'.format(msg[0],a=Attr)) 79 # XXX get the commits in the merge from the actual commit data instead of from the commit message 80 commits = [] 81 for line in msg[2:]: 82 if not line: # stop at first empty line 83 break 84 cid,_,message = line.partition(' ') 85 commits.append((cid,message)) 86 87 for (cid, message) in reversed(commits): 88 print('{a.hsh}# {a.head2}{}{a.reset}'.format(cid + ' '+ message,a=Attr)) 89 commit = repo.commit(cid) 90 cmsg = commit.message 91 cmsg += '\n' 92 cmsg += 'Github-Pull: %s\n' % t[0] 93 cmsg += 'Rebased-From: %s\n' % commit.hexsha 94 if execute: 95 if subprocess.call([GIT,'cherry-pick', commit.hexsha]): 96 # fail - drop to shell 97 print('Dropping to shell - run git cherry-pick --continue after fixing issues, or exit and choose abort/skip') 98 if os.path.isfile('/etc/debian_version'): # Show pull number on Debian default prompt 99 os.putenv('debian_chroot',t[0]) 100 subprocess.call([BASH,'-i']) 101 reply = ask_prompt("Type 'c' to continue, 'a' to abort, 's' to skip pull.") 102 if reply == 'c': 103 pass 104 elif reply == 'a': 105 exit(1) 106 elif reply == 's': 107 subprocess.check_call([GIT,'cherry-pick', '--abort']) 108 continue 109 110 # Sign 111 subprocess.check_call([GIT,'commit','--amend','--gpg-sign','-q','-m',cmsg]) 112 else: 113 print('git cherry-pick %s' % (commit.hexsha)) 114 print('git commit -q --amend -m %s' % (shlex.quote(cmsg)))