]> git.wh0rd.org - home.git/blob - .bin/git-rb-catchup
bin: improve rebase helpers
[home.git] / .bin / git-rb-catchup
1 #!/usr/bin/env python3
2
3 """Helper to automatically rebase onto latest commit possible."""
4
5 import argparse
6 import subprocess
7 import sys
8
9
10 def git(args, **kwargs):
11 """Run git."""
12 kwargs.setdefault('check', True)
13 kwargs.setdefault('capture_output', True)
14 kwargs.setdefault('encoding', 'utf-8')
15 return subprocess.run(['git'] + args, **kwargs)
16
17
18 def rebase(target):
19 """Try to rebase onto |target|."""
20 try:
21 git(['rebase', target])
22 return True
23 except KeyboardInterrupt:
24 git(['rebase', '--abort'])
25 print('aborted')
26 sys.exit(1)
27 except:
28 git(['rebase', '--abort'])
29 return False
30
31
32 def rebase_bisect(lbranch, rbranch, behind):
33 """Try to rebase branch as close to |rbranch| as possible."""
34 def attempt(pos):
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)
42 ret = rebase(target)
43 print('OK' if ret else 'failed')
44 return ret
45
46 #"min" is the latest branch commit while "max" is where we're now.
47 min = 0
48 max = behind
49 old_mid = None
50 while True:
51 mid = min + (max - min) // 2
52 if mid == old_mid or mid < min or mid >= max:
53 break
54 if attempt(mid):
55 max = mid
56 else:
57 min = mid
58 old_mid = mid
59 print('Done')
60
61
62 def get_ahead_behind(lbranch, rbranch):
63 """Return number of commits |lbranch| is ahead & behind relative to |rbranch|."""
64 output = git(['rev-list', '--left-right', '--count', f'{lbranch}...{rbranch}']).stdout
65 return [int(x) for x in output.split()]
66
67
68 def get_tracking_branch(branch):
69 """Return remote branch that |branch| is tracking."""
70 merge = git(['config', '--local', f'branch.{branch}.merge']).stdout.strip()
71 if not merge:
72 return None
73
74 remote = git(['config', '--local', f'branch.{branch}.remote']).stdout.strip()
75 if remote:
76 if merge.startswith('refs/heads/'):
77 merge = merge[11:]
78 return f'{remote}/{merge}'
79 else:
80 return merge
81
82
83 def get_local_branch():
84 """Return the name of the local checked out branch."""
85 return git(['branch', '--show-current']).stdout.strip()
86
87
88 def get_parser():
89 """Get CLI parser."""
90 parser = argparse.ArgumentParser(description=__doc__)
91 parser.add_argument(
92 '--skip-initial-rebase-latest', dest='initial_rebase',
93 action='store_false', default=True,
94 help='skip initial rebase attempt onto the latest branch')
95 parser.add_argument(
96 'branch', nargs='?',
97 help='branch to rebase onto')
98 return parser
99
100
101 def main(argv):
102 """The main entry point for scripts."""
103 parser = get_parser()
104 opts = parser.parse_args(argv)
105
106 lbranch = get_local_branch()
107 print(f'Local branch resolved to "{lbranch}"')
108 if not lbranch:
109 print('Unable to resolve local branch', file=sys.stderr)
110 return 1
111
112 if opts.branch:
113 rbranch = opts.branch
114 else:
115 rbranch = get_tracking_branch(lbranch)
116 print(f'Remote branch resolved to "{rbranch}"')
117
118 ahead, behind = get_ahead_behind(lbranch, rbranch)
119 print(f'Branch is {ahead} commits ahead and {behind} commits behind')
120
121 if not behind:
122 print('Up-to-date!')
123 elif not ahead:
124 print('Fast forwarding ...')
125 git(['merge'])
126 else:
127 if opts.initial_rebase:
128 print(f'Trying to rebase onto latest {rbranch} ... ', end='', flush=True)
129 if rebase(rbranch):
130 print('OK!')
131 return 0
132 print('failed; falling back to bisect')
133 rebase_bisect(lbranch, rbranch, behind)
134
135
136 if __name__ == '__main__':
137 sys.exit(main(sys.argv[1:]))