]>
Commit | Line | Data |
---|---|---|
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:])) |