3 from pathlib import Path as P
4 from urllib.parse import urlparse
5 from contextlib import contextmanager
18 URL = "https://gitlab.freedesktop.org/"
19 PARSER = argparse.ArgumentParser(
20 description="`Rebase` a branch from an old GStreamer module onto the monorepo"
22 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")
23 PARSER.add_argument("branch", help="The branch to rebase.")
25 log_depth = [] # type: T.List[str]
31 log_depth.append(name)
39 return f"\033[1m{text}\033[0m"
43 return f"\033[1;32m{text}\033[0m"
47 return f"\033[1;31m{text}\033[0m"
50 def yellow(text: str):
51 return f"\033[1;33m{text}\033[0m"
54 def fprint(msg, nested=True):
56 prepend = log_depth[-1] + ' | ' if nested else ''
60 print(prepend + msg, end="")
64 class GstCherryPicker:
71 self.git_rename_limit = None
73 def check_clean(self):
75 out = self.git("status", "--porcelain")
77 fprint("\n" + red('Git repository is not clean:') + "\n```\n" + out + "\n```\n")
80 except Exception as e:
82 f"Git repository is not clean. Clean it up before running ({e})")
90 git_rename_limit = int(self.git("config", "merge.renameLimit"))
91 except subprocess.CalledProcessError:
93 if int(git_rename_limit) < 999999:
94 self.git_rename_limit = git_rename_limit
95 fprint("-> Setting git rename limit to 999999 so we can properly cherry-pick between repos")
96 self.git("config", "merge.renameLimit", "999999")
97 fprint(f"{green(' OK')}\n", nested=False)
102 if self.git_rename_limit is not None:
103 self.git("config", "merge.renameLimit", str(self.git_rename_limit))
106 repo = urlparse(self.repo)
108 repo_path = P(repo.path)
109 self.module = module = repo_path.stem
110 remote_name = f"{module}-{repo_path.parent.name}"
111 fprint('Adding remotes...')
112 self.git("remote", "add", remote_name, self.repo, can_fail=True)
113 self.git("remote", "add", module, f"{URL}gstreamer/{module}.git",
115 fprint(f"{green(' OK')}\n", nested=False)
117 fprint(f'Fetching {remote_name}...')
118 self.git("fetch", remote_name,
119 interaction_message=f"fetching {remote_name} with:\n"
120 f" `$ git fetch {remote_name}`")
121 fprint(f"{green(' OK')}\n", nested=False)
123 fprint(f'Fetching {module}...')
124 self.git("fetch", module,
125 interaction_message=f"fetching {module} with:\n"
126 f" `$ git fetch {module}`")
127 fprint(f"{green(' OK')}\n", nested=False)
129 prevbranch = self.git("rev-parse", "--abbrev-ref", "HEAD").strip()
130 tmpbranchname = f"{remote_name}_{self.branch}"
131 fprint(f'Checking out branch {remote_name}/{self.branch} as {tmpbranchname}\n')
133 self.git("checkout", f"{remote_name}/{self.branch}", "-b", tmpbranchname)
134 self.git("rebase", f"{module}/master",
135 interaction_message=f"Failed rebasing {remote_name}/{self.branch} on {module}/master with:\n"
136 f" `$ git rebase {module}/master`")
137 ret = self.cherry_pick(tmpbranchname)
138 except Exception as e:
139 self.git("rebase", "--abort", can_fail=True)
140 self.git("checkout", prevbranch)
141 self.git("branch", "-D", tmpbranchname)
144 fprint(f"{green(' OK')}\n", nested=False)
146 self.git("checkout", prevbranch)
147 self.git("branch", "-D", tmpbranchname)
148 fprint(f"{red(' ERROR')}\n", nested=False)
150 def cherry_pick(self, branch):
151 shas = self.git('log', '--format=format:%H', f'{self.module}/master..').strip()
152 fprint(f'Resetting on origin/main')
153 self.git("reset", "--hard", "origin/main")
154 fprint(f"{green(' OK')}\n", nested=False)
156 for sha in reversed(shas.split()):
157 fprint(f' - Cherry picking: {bold(sha)}\n')
159 self.git("cherry-pick", sha,
160 interaction_message=f"cherry-picking {sha} onto {branch} with:\n "
161 f" `$ git cherry-pick {sha}`",
162 revert_operation=["cherry-pick", "--abort"])
163 except Exception as e:
164 fprint(f' - Cherry picking failed: {bold(sha)}\n')
168 def git(self, *args, can_fail=False, interaction_message=None, call=False, revert_operation=None):
175 return subprocess.check_output(["git"] + list(args),
176 stdin=subprocess.DEVNULL,
177 stderr=subprocess.STDOUT).decode()
178 except Exception as e:
180 fprint(f"\n\n{bold(red('ERROR'))}: `git {' '.join(args)}` failed" + "\n", nested=False)
183 subprocess.call(["git"] + list(args))
185 except Exception as e:
186 if interaction_message:
187 output = getattr(e, "output", b"")
188 if output is not None:
189 out = output.decode()
194 f"Entering a shell to fix:\n\n"
195 f" {bold(interaction_message)}\n\n"
196 f"You should then exit with the following codes:\n\n"
197 f" - {bold('`exit 0`')}: once you have fixed the problem and we can keep moving the \n"
198 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"
199 f" - {bold('`exit 2`')}: stop the script and abandon rebasing your branch\n"
200 "\n```\n", nested=False)
203 shell = os.environ.get(
204 "COMSPEC", r"C:\WINDOWS\system32\cmd.exe")
206 shell = os.environ.get(
207 "SHELL", os.path.realpath("/bin/sh"))
208 subprocess.check_call(shell)
209 except subprocess.CalledProcessError as e:
210 if e.returncode == 1:
213 elif e.returncode == 2:
215 self.git(*revert_operation, can_fail=True)
217 except Exception as e:
218 # Result of subshell does not really matter
221 return "User fixed it"
224 return "Failed but we do not care"
230 picker = GstCherryPicker()
231 PARSER.parse_args(namespace=picker)
235 if __name__ == '__main__':