/ 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)))