]> git.wh0rd.org - home.git/blob - .bin/git-rb-all
1d9ded3d9d0786b9cd6060223b1a0de4d94fd3ce
[home.git] / .bin / git-rb-all
1 #!/usr/bin/env python3
2
3 """Helper to rebase all local branches."""
4
5 import argparse
6 import os
7 from pathlib import Path
8 import re
9 import subprocess
10 import sys
11
12
13 PROG = os.path.basename(__file__)
14
15
16 class Terminal:
17 """Terminal escape sequences."""
18
19 CSI_PREFIX = '\033['
20 SGR_SUFFIX = 'm'
21 NORMAL = ''
22 BOLD = '1'
23 _BLACK, _RED, _GREEN, _YELLOW, _BLUE, _MAGENTA, _CYAN, _WHITE = range(0, 8)
24 _FG = 30
25 _BG = 40
26 FG_BLACK = str(_FG + _BLACK)
27 FG_RED = str(_FG + _RED)
28 FG_GREEN = str(_FG + _GREEN)
29 FG_YELLOW = str(_FG + _YELLOW)
30 FG_BLUE = str(_FG + _BLUE)
31 FG_MAGENTA = str(_FG + _MAGENTA)
32 FG_CYAN = str(_FG + _CYAN)
33 FG_WHITE = str(_FG + _WHITE)
34
35
36 class Color:
37 """Helper colors."""
38
39 _combine = lambda *args: Terminal.CSI_PREFIX + ';'.join(args) + Terminal.SGR_SUFFIX
40 NORMAL = _combine(Terminal.NORMAL)
41 GOOD = _combine(Terminal.FG_GREEN)
42 WARN = _combine(Terminal.FG_YELLOW)
43 BAD = _combine(Terminal.FG_RED)
44 HILITE = _combine(Terminal.BOLD, Terminal.FG_CYAN)
45 BRACKET = _combine(Terminal.BOLD, Terminal.FG_BLUE)
46
47
48 def git(args, **kwargs):
49 """Run git."""
50 kwargs.setdefault('check', True)
51 kwargs.setdefault('capture_output', True)
52 kwargs.setdefault('encoding', 'utf-8')
53 #print('+', 'git', *args)
54 return subprocess.run(['git'] + args, **kwargs)
55
56
57 def rebase_inprogress():
58 """Determine whether a rebase is already in progress."""
59 output = git(['rev-parse', '--git-path', 'rebase-merge']).stdout.strip()
60 if Path(output).exists():
61 return True
62
63 output = git(['rev-parse', '--git-path', 'rebase-apply']).stdout.strip()
64 if Path(output).exists():
65 return True
66
67 return False
68
69
70 class AppendOption(argparse.Action):
71 """Append the command line option (with no arguments) to dest.
72
73 parser.add_argument('-b', '--barg', dest='out', action='append_option')
74 options = parser.parse_args(['-b', '--barg'])
75 options.out == ['-b', '--barg']
76 """
77
78 def __init__(self, option_strings, dest, **kwargs):
79 if 'nargs' in kwargs:
80 raise ValueError('nargs is not supported for append_option action')
81 super().__init__(option_strings, dest, nargs=0, **kwargs)
82
83 def __call__(self, parser, namespace, values, option_string=None):
84 if getattr(namespace, self.dest, None) is None:
85 setattr(namespace, self.dest, [])
86 getattr(namespace, self.dest).append(option_string)
87
88
89 def get_parser():
90 """Get CLI parser."""
91 parser = argparse.ArgumentParser(description=__doc__)
92 parser.add_argument(
93 '--catchup', action='store_true',
94 help='run git-rb-catchup when rebasing')
95 parser.add_argument(
96 '-q', '--quiet', dest='git_options', action=AppendOption, default=['-q'],
97 help='passthru to git rebase')
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 # Skip if rebase is in progress.
107 if rebase_inprogress():
108 print(f'{Color.BAD}{PROG}: skipping due to active rebase{Color.NORMAL}')
109 return 1
110
111 # Switch to the top dir in case the working dir doesn't exist in every branch.
112 topdir = git(['rev-parse', '--show-toplevel']).stdout.strip()
113 os.chdir(topdir)
114
115 # Example output:
116 # ||m|refs/remotes/origin/master|ahead 2, behind 203
117 # *||master|refs/remotes/origin/master|ahead 1
118 # |/usr/local/src/gnu/gdb/build/release|release||behind 10
119 # ||s-stash||
120 state = git(
121 ['for-each-ref', '--format=%(HEAD)|%(worktreepath)|%(refname:short)|%(upstream)|%(upstream:track,nobracket)',
122 'refs/heads/*']).stdout.splitlines()
123
124 curr_state = None
125 branch_width = 0
126 for line in state:
127 head, worktreepath, branch, tracking, ahead_behind = line.split('|')
128 branch_width = max(branch_width, len(branch))
129 if head == '*':
130 curr_state = branch
131 if not curr_state:
132 print('Unable to resolve current branch', file=sys.stderr)
133 return 1
134
135 branches = {}
136 for line in state:
137 head, worktreepath, branch, tracking, ahead_behind = line.split('|')
138
139 # If it's a branch in another worktree, ignore it.
140 if worktreepath and worktreepath != topdir:
141 continue
142
143 print(f'{Color.BRACKET}### {Color.GOOD}{branch:{branch_width}}{Color.NORMAL} ',
144 end='', flush=True)
145 if not tracking:
146 print(f'{Color.WARN}skipping{Color.NORMAL} due to missing merge branch')
147 continue
148
149 m = re.match(r'ahead ([0-9]+)', ahead_behind)
150 ahead = int(m.group(1)) if m else 0
151 m = re.search(r'behind ([0-9]+)', ahead_behind)
152 behind = int(m.group(1)) if m else 0
153 if not behind:
154 print('Up-to-date!')
155 continue
156 elif not ahead:
157 git(['update-ref', f'refs/heads/{branch}', tracking])
158 print('fast forwarded')
159 continue
160
161 print(f'rebasing [{ahead_behind}] ', end='', flush=True)
162 git(['checkout', '-q', branch])
163 if opts.catchup:
164 print()
165 result = git(['rb-catchup'], capture_output=False, check=False)
166 else:
167 result = git(['rebase'] + opts.git_options, check=False)
168 if result.returncode:
169 git(['rebase', '--abort'])
170 print(f'{Color.BAD}failed{Color.NORMAL}\n' + result.stdout.strip())
171 else:
172 print('OK!')
173
174 git(['checkout', '-q', curr_state])
175 return 0
176
177
178 if __name__ == '__main__':
179 sys.exit(main(sys.argv[1:]))