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