2 # Distributed under the terms of the GNU General Public License v2 or later.
4 """Helper to automatically rebase onto latest commit possible.
6 Helpful when you have a branch tracking an old commit, and a lot of conflicting
7 changes have landed in the latest branch, but you still want to update.
9 A single rebase to the latest commit will require addressing all the different
10 changes at once which can be difficult, overwhelming, and error-prone. Instead,
11 if you rebased onto each intermediate conflicting point, you'd break up the work
12 into smaller pieces, and be able to run tests to make sure things were still OK.
18 from typing
import List
, Tuple
, Union
21 assert sys
.version_info
>= (3, 7), f
'Need Python 3.7+, not {sys.version_info}'
24 def git(args
: List
[str], **kwargs
) -> subprocess
.CompletedProcess
:
26 kwargs
.setdefault('check', True)
27 kwargs
.setdefault('capture_output', True)
28 kwargs
.setdefault('encoding', 'utf-8')
29 # pylint: disable=subprocess-run-check
30 return subprocess
.run(['git'] + args
, **kwargs
)
33 def rebase(target
: str) -> bool:
34 """Try to rebase onto |target|."""
36 git(['rebase', target
])
38 except KeyboardInterrupt:
39 git(['rebase', '--abort'])
43 git(['rebase', '--abort'])
47 def rebase_bisect(lbranch
: str,
50 leave_rebase
: bool = False,
51 force_checkout
: bool = False):
52 """Try to rebase branch as close to |rbranch| as possible."""
53 def attempt(pos
: int) -> bool:
54 target
= f
'{rbranch}~{pos}'
55 print(f
'Rebasing onto {target} ', end
='')
56 print('.', end
='', flush
=True)
57 # Checking out these branches directly helps clobber orphaned files,
58 # but is usually unnessary, and can slow down the overall process.
60 git(['checkout', '-f', target
])
61 print('.', end
='', flush
=True)
63 git(['checkout', '-f', lbranch
])
64 print('. ', end
='', flush
=True)
66 print('OK' if ret
else 'failed')
69 # "pmin" is the latest branch position while "pmax" is where we're now.
75 mid
= pmin
+ (pmax
- pmin
) // 2
76 if mid
== old_mid
or mid
< pmin
or mid
>= pmax
:
81 first_fail
= max(first_fail
, mid
)
86 last_target
= f
'{rbranch}~{first_fail}'
88 print('Restarting', last_target
)
89 result
= git(['rebase', last_target
], check
=False)
90 print(result
.stdout
.strip())
92 print('Found first failure', last_target
)
94 print('All caught up!')
97 def get_ahead_behind(lbranch
: str, rbranch
: str) -> Tuple
[int, int]:
98 """Return number of commits |lbranch| is ahead & behind relative to |rbranch|."""
100 ['rev-list', '--left-right', '--count', f
'{lbranch}...{rbranch}']).stdout
101 return [int(x
) for x
in output
.split()]
104 def get_tracking_branch(branch
: str) -> Union
[str, None]:
105 """Return branch that |branch| is tracking."""
106 merge
= git(['config', '--local', f
'branch.{branch}.merge']).stdout
.strip()
110 remote
= git(['config', '--local', f
'branch.{branch}.remote']).stdout
.strip()
112 if merge
.startswith('refs/heads/'):
114 return f
'{remote}/{merge}'
119 def get_local_branch() -> str:
120 """Return the name of the local checked out branch."""
121 return git(['branch', '--show-current']).stdout
.strip()
124 def get_parser() -> argparse
.ArgumentParser
:
125 """Get CLI parser."""
126 parser
= argparse
.ArgumentParser(
128 formatter_class
=argparse
.RawDescriptionHelpFormatter
)
130 '--skip-initial-rebase-latest', dest
='initial_rebase',
131 action
='store_false', default
=True,
132 help='skip initial rebase attempt onto the latest branch')
134 '--leave-at-last-failed-rebase', dest
='leave_rebase',
135 action
='store_true', default
=False,
136 help='leave tree state at last failing rebase')
138 '--checkout-before-rebase', dest
='force_checkout',
139 action
='store_true', default
=False,
140 help='force checkout before rebasing to target (to cleanup orphans)')
143 help='branch to rebase onto')
147 def main(argv
: List
[str]) -> int:
148 """The main entry point for scripts."""
149 parser
= get_parser()
150 opts
= parser
.parse_args(argv
)
152 lbranch
= get_local_branch()
153 print(f
'Local branch resolved to "{lbranch}"')
155 print('Unable to resolve local branch', file=sys
.stderr
)
159 rbranch
= opts
.branch
161 rbranch
= get_tracking_branch(lbranch
)
162 print(f
'Tracking branch resolved to "{rbranch}"')
164 ahead
, behind
= get_ahead_behind(lbranch
, rbranch
)
165 print(f
'Branch is {ahead} commits ahead and {behind} commits behind')
170 print('Fast forwarding ...')
173 if opts
.initial_rebase
:
174 print(f
'Trying to rebase onto latest {rbranch} ... ',
179 print('failed; falling back to bisect')
180 rebase_bisect(lbranch
, rbranch
, behind
, leave_rebase
=opts
.leave_rebase
,
181 force_checkout
=opts
.force_checkout
)
186 if __name__
== '__main__':
187 sys
.exit(main(sys
.argv
[1:]))