3 """Helper to automatically rebase onto latest commit possible."""
10 def git(args, **kwargs):
12 kwargs.setdefault('check', True)
13 kwargs.setdefault('capture_output', True)
14 kwargs.setdefault('encoding', 'utf-8')
15 return subprocess.run(['git'] + args, **kwargs)
19 """Try to rebase onto |target|."""
21 git(['rebase', target])
23 except KeyboardInterrupt:
24 git(['rebase', '--abort'])
28 git(['rebase', '--abort'])
32 def rebase_bisect(lbranch, rbranch, behind, leave_rebase=False):
33 """Try to rebase branch as close to |rbranch| as possible."""
35 target = f'{rbranch}~{pos}'
36 print(f'Rebasing onto {target} ', end='', flush=True)
37 print('.', end='', flush=True)
38 # git(['checkout', '-f', target])
39 print('.', end='', flush=True)
40 # git(['checkout', '-f', lbranch])
41 print('. ', end='', flush=True)
43 print('OK' if ret else 'failed')
46 # "pmin" is the latest branch position while "pmax" is where we're now.
52 mid = pmin + (pmax - pmin) // 2
53 if mid == old_mid or mid < pmin or mid >= pmax:
58 first_fail = max(first_fail, mid)
63 last_target = f'{rbranch}~{first_fail}'
65 print('Restarting', last_target)
66 result = git(['rebase', last_target], check=False)
67 print(result.stdout.strip())
69 print('Found first failure', last_target)
71 print('All caught up!')
74 def get_ahead_behind(lbranch, rbranch):
75 """Return number of commits |lbranch| is ahead & behind relative to |rbranch|."""
76 output = git(['rev-list', '--left-right', '--count', f'{lbranch}...{rbranch}']).stdout
77 return [int(x) for x in output.split()]
80 def get_tracking_branch(branch):
81 """Return remote branch that |branch| is tracking."""
82 merge = git(['config', '--local', f'branch.{branch}.merge']).stdout.strip()
86 remote = git(['config', '--local', f'branch.{branch}.remote']).stdout.strip()
88 if merge.startswith('refs/heads/'):
90 return f'{remote}/{merge}'
95 def get_local_branch():
96 """Return the name of the local checked out branch."""
97 return git(['branch', '--show-current']).stdout.strip()
101 """Get CLI parser."""
102 parser = argparse.ArgumentParser(description=__doc__)
104 '--skip-initial-rebase-latest', dest='initial_rebase',
105 action='store_false', default=True,
106 help='skip initial rebase attempt onto the latest branch')
108 '--leave-at-last-failed-rebase', dest='leave_rebase',
109 action='store_true', default=False,
110 help='leave tree state at last failing rebase')
113 help='branch to rebase onto')
118 """The main entry point for scripts."""
119 parser = get_parser()
120 opts = parser.parse_args(argv)
122 lbranch = get_local_branch()
123 print(f'Local branch resolved to "{lbranch}"')
125 print('Unable to resolve local branch', file=sys.stderr)
129 rbranch = opts.branch
131 rbranch = get_tracking_branch(lbranch)
132 print(f'Remote branch resolved to "{rbranch}"')
134 ahead, behind = get_ahead_behind(lbranch, rbranch)
135 print(f'Branch is {ahead} commits ahead and {behind} commits behind')
140 print('Fast forwarding ...')
143 if opts.initial_rebase:
144 print(f'Trying to rebase onto latest {rbranch} ... ', end='', flush=True)
148 print('failed; falling back to bisect')
149 rebase_bisect(lbranch, rbranch, behind, leave_rebase=opts.leave_rebase)
152 if __name__ == '__main__':
153 sys.exit(main(sys.argv[1:]))