#!/usr/bin/env python3 import bisect import functools import shlex import subprocess as s import sys devel = False tokens = """ git merge-base rev-parse HEAD rev-list --ancestry-path -b --abort symbolic-ref --short --no-commit stash pop --get config rebase.autostash """ help = """ Seems safe, but use at your own risk. Brase will try merge/rebase a branch as far as possible using bisect. Please specify which branch you want to merge/rebase. git brase '' [-n] --exec execute the merge/rebase found """.strip() class Tokens: def __init__(self): token_list = shlex.split(tokens) token_dict = {} for token in token_list: name = token.lstrip("-") name = name.replace("-", "_") name = name.replace(".", "_") token_dict[name] = token assert len(token_dict.keys()) == len(token_list) for name in token_dict.keys(): setattr(self, name, token_dict[name]) t = Tokens() def git(*args): try: return s.check_output((t.git,) + args, stderr=s.STDOUT).strip().decode("UTF-8") except s.CalledProcessError as e: if devel: print(get_output(e)) raise def igit(*args): try: return git(*args) except s.CalledProcessError as e: if devel: print(get_output(e)) import traceback traceback.print_exc() def get_output(e): return e.output.decode("UTF-8") def execute(target, index): ret = False try: git(t.command, t.no_commit, target[index]) ret = True except s.CalledProcessError: pass finally: igit(t.command, t.abort) short = target[index][:8] if ret: text = "sucessful" else: text = "failed" print(f"{t.command} index({index}) rev({short}) {text}") return ret def brase(target, base): max_index = len(target) - 1 driver = functools.partial(execute, target) class BisectTarget(object): def __getitem__(self, index): ret = driver(index) return ret def __len__(self): return max_index if driver(0): return 0 else: bt = BisectTarget() return bisect.bisect(bt, False) def main(): head = git(t.rev_parse, t.HEAD) head_name = git(t.symbolic_ref, t.short, t.HEAD) dry = True len_argv = len(sys.argv) if len_argv == 4: if sys.argv[3] != "--exec": print(help) sys.exit(1) else: dry = False elif len_argv < 3: print(help) sys.exit(1) elif sys.argv[1] not in ("merge", "rebase"): print(help) sys.exit(1) t.command = sys.argv[1] target_name = sys.argv[2] target = git(t.rev_parse, target_name) assert target != head base = git(t.merge_base, head, target) path = git(t.rev_list, t.ancestry_path, f"{base}..{target}").split("\n") print(f"found {len(path)} commits to {t.command}") # _path_parent = git(t.rev_parse, f"{path[-1]}^") assert path[0] == target # assert _path_parent == base autostash = git(t.config, t.get, t.rebase_autostash).lower() == "true" stashed = False if autostash and t.command == "rebase": try: if not git(t.stash): stashed = True except s.CalledProcessError: pass try: index = brase(path, base) if dry: sys.stdout.write("-> ") execute(path, index) finally: igit(t.command, t.abort) if stashed: git(t.stash, t.pop) if not dry: try: git(t.command, path[index]) except s.CalledProcessError as e: print(get_output(e)) if __name__ == "__main__": main()