| #!/usr/bin/env python3 | |
| from pathlib import Path as P | |
| from urllib.parse import urlparse | |
| from contextlib import contextmanager | |
| import os | |
| import re | |
| import sys | |
| import argparse | |
| import requests | |
| import subprocess | |
| import random | |
| import string | |
| URL = "https://gitlab.freedesktop.org/" | |
| PARSER = argparse.ArgumentParser( | |
| description="`Rebase` a branch from an old GStreamer module onto the monorepo" | |
| ) | |
| PARSER.add_argument("repo", help="The repo with the old module to use. ie https://gitlab.freedesktop.org/user/gst-plugins-bad.git or /home/me/gst-build/subprojects/gst-plugins-bad") | |
| PARSER.add_argument("branch", help="The branch to rebase.") | |
| log_depth = [] # type: T.List[str] | |
| @contextmanager | |
| def nested(name=''): | |
| global log_depth | |
| log_depth.append(name) | |
| try: | |
| yield | |
| finally: | |
| log_depth.pop() | |
| def bold(text: str): | |
| return f"\033[1m{text}\033[0m" | |
| def green(text: str): | |
| return f"\033[1;32m{text}\033[0m" | |
| def red(text: str): | |
| return f"\033[1;31m{text}\033[0m" | |
| def yellow(text: str): | |
| return f"\033[1;33m{text}\033[0m" | |
| def fprint(msg, nested=True): | |
| if log_depth: | |
| prepend = log_depth[-1] + ' | ' if nested else '' | |
| else: | |
| prepend = '' | |
| print(prepend + msg, end="") | |
| sys.stdout.flush() | |
| class GstCherryPicker: | |
| def __init__(self): | |
| self.branch = None | |
| self.repo = None | |
| self.module = None | |
| self.git_rename_limit = None | |
| def check_clean(self): | |
| try: | |
| out = self.git("status", "--porcelain") | |
| if out: | |
| fprint("\n" + red('Git repository is not clean:') + "\n```\n" + out + "\n```\n") | |
| sys.exit(1) | |
| except Exception as e: | |
| sys.exit( | |
| f"Git repository is not clean. Clean it up before running ({e})") | |
| def run(self): | |
| assert self.branch | |
| assert self.repo | |
| self.check_clean() | |
| try: | |
| git_rename_limit = int(self.git("config", "merge.renameLimit")) | |
| except subprocess.CalledProcessError: | |
| git_rename_limit = 0 | |
| if int(git_rename_limit) < 999999: | |
| self.git_rename_limit = git_rename_limit | |
| fprint("-> Setting git rename limit to 999999 so we can properly cherry-pick between repos") | |
| self.git("config", "merge.renameLimit", "999999") | |
| fprint(f"{green(' OK')}\n", nested=False) | |
| try: | |
| self.rebase() | |
| finally: | |
| if self.git_rename_limit is not None: | |
| self.git("config", "merge.renameLimit", str(self.git_rename_limit)) | |
| def rebase(self): | |
| repo = urlparse(self.repo) | |
| repo_path = P(repo.path) | |
| self.module = module = repo_path.stem | |
| remote_name = f"{module}-{repo_path.parent.name}" | |
| fprint('Adding remotes...') | |
| self.git("remote", "add", remote_name, self.repo, can_fail=True) | |
| self.git("remote", "add", module, f"{URL}gstreamer/{module}.git", | |
| can_fail=True) | |
| fprint(f"{green(' OK')}\n", nested=False) | |
| fprint(f'Fetching {remote_name}...') | |
| self.git("fetch", remote_name, | |
| interaction_message=f"fetching {remote_name} with:\n" | |
| f" `$ git fetch {remote_name}`") | |
| fprint(f"{green(' OK')}\n", nested=False) | |
| fprint(f'Fetching {module}...') | |
| self.git("fetch", module, | |
| interaction_message=f"fetching {module} with:\n" | |
| f" `$ git fetch {module}`") | |
| fprint(f"{green(' OK')}\n", nested=False) | |
| prevbranch = self.git("rev-parse", "--abbrev-ref", "HEAD").strip() | |
| tmpbranchname = f"{remote_name}_{self.branch}" | |
| fprint(f'Checking out branch {remote_name}/{self.branch} as {tmpbranchname}\n') | |
| try: | |
| self.git("checkout", f"{remote_name}/{self.branch}", "-b", tmpbranchname) | |
| self.git("rebase", f"{module}/master", | |
| interaction_message=f"Failed rebasing {remote_name}/{self.branch} on {module}/master with:\n" | |
| f" `$ git rebase {module}/master`") | |
| ret = self.cherry_pick(tmpbranchname) | |
| except Exception as e: | |
| self.git("rebase", "--abort", can_fail=True) | |
| self.git("checkout", prevbranch) | |
| self.git("branch", "-D", tmpbranchname) | |
| raise | |
| if ret: | |
| fprint(f"{green(' OK')}\n", nested=False) | |
| else: | |
| self.git("checkout", prevbranch) | |
| self.git("branch", "-D", tmpbranchname) | |
| fprint(f"{red(' ERROR')}\n", nested=False) | |
| def cherry_pick(self, branch): | |
| shas = self.git('log', '--format=format:%H', f'{self.module}/master..').strip() | |
| fprint(f'Resetting on origin/main') | |
| self.git("reset", "--hard", "origin/main") | |
| fprint(f"{green(' OK')}\n", nested=False) | |
| for sha in reversed(shas.split()): | |
| fprint(f' - Cherry picking: {bold(sha)}\n') | |
| try: | |
| self.git("cherry-pick", sha, | |
| interaction_message=f"cherry-picking {sha} onto {branch} with:\n " | |
| f" `$ git cherry-pick {sha}`", | |
| revert_operation=["cherry-pick", "--abort"]) | |
| except Exception as e: | |
| fprint(f' - Cherry picking failed: {bold(sha)}\n') | |
| return False | |
| return True | |
| def git(self, *args, can_fail=False, interaction_message=None, call=False, revert_operation=None): | |
| retry = True | |
| while retry: | |
| retry = False | |
| try: | |
| if not call: | |
| try: | |
| return subprocess.check_output(["git"] + list(args), | |
| stdin=subprocess.DEVNULL, | |
| stderr=subprocess.STDOUT).decode() | |
| except Exception as e: | |
| if not can_fail: | |
| fprint(f"\n\n{bold(red('ERROR'))}: `git {' '.join(args)}` failed" + "\n", nested=False) | |
| raise | |
| else: | |
| subprocess.call(["git"] + list(args)) | |
| return "All good" | |
| except Exception as e: | |
| if interaction_message: | |
| output = getattr(e, "output", b"") | |
| if output is not None: | |
| out = output.decode() | |
| else: | |
| out = "????" | |
| fprint(f"\n```" | |
| f"\n{out}\n" | |
| f"Entering a shell to fix:\n\n" | |
| f" {bold(interaction_message)}\n\n" | |
| f"You should then exit with the following codes:\n\n" | |
| f" - {bold('`exit 0`')}: once you have fixed the problem and we can keep moving the \n" | |
| f" - {bold('`exit 1`')}: {bold('retry')}: once you have let the repo in a state where cherry-picking the commit should be to retried\n" | |
| f" - {bold('`exit 2`')}: stop the script and abandon rebasing your branch\n" | |
| "\n```\n", nested=False) | |
| try: | |
| if os.name == 'nt': | |
| shell = os.environ.get( | |
| "COMSPEC", r"C:\WINDOWS\system32\cmd.exe") | |
| else: | |
| shell = os.environ.get( | |
| "SHELL", os.path.realpath("/bin/sh")) | |
| subprocess.check_call(shell) | |
| except subprocess.CalledProcessError as e: | |
| if e.returncode == 1: | |
| retry = True | |
| continue | |
| elif e.returncode == 2: | |
| if revert_operation: | |
| self.git(*revert_operation, can_fail=True) | |
| raise | |
| except Exception as e: | |
| # Result of subshell does not really matter | |
| pass | |
| return "User fixed it" | |
| if can_fail: | |
| return "Failed but we do not care" | |
| raise e | |
| def main(): | |
| picker = GstCherryPicker() | |
| PARSER.parse_args(namespace=picker) | |
| picker.run() | |
| if __name__ == '__main__': | |
| main() |