Back to development
[platform/upstream/gstreamer.git] / scripts / rebase-branch-from-old-module.py
1 #!/usr/bin/env python3
2
3 from pathlib import Path as P
4 from urllib.parse import urlparse
5 from contextlib import contextmanager
6 import os
7 import re
8 import sys
9
10 import argparse
11 import requests
12
13 import subprocess
14
15 import random
16 import string
17
18 URL = "https://gitlab.freedesktop.org/"
19 PARSER = argparse.ArgumentParser(
20     description="`Rebase` a branch from an old GStreamer module onto the monorepo"
21 )
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.")
24
25 log_depth = []               # type: T.List[str]
26
27
28 @contextmanager
29 def nested(name=''):
30     global log_depth
31     log_depth.append(name)
32     try:
33         yield
34     finally:
35         log_depth.pop()
36
37
38 def bold(text: str):
39     return f"\033[1m{text}\033[0m"
40
41
42 def green(text: str):
43     return f"\033[1;32m{text}\033[0m"
44
45
46 def red(text: str):
47     return f"\033[1;31m{text}\033[0m"
48
49
50 def yellow(text: str):
51     return f"\033[1;33m{text}\033[0m"
52
53
54 def fprint(msg, nested=True):
55     if log_depth:
56         prepend = log_depth[-1] + ' | ' if nested else ''
57     else:
58         prepend = ''
59
60     print(prepend + msg, end="")
61     sys.stdout.flush()
62
63
64 class GstCherryPicker:
65     def __init__(self):
66
67         self.branch = None
68         self.repo = None
69         self.module = None
70
71         self.git_rename_limit = None
72
73     def check_clean(self):
74         try:
75             out = self.git("status", "--porcelain")
76             if out:
77                 fprint("\n" + red('Git repository is not clean:') + "\n```\n" + out + "\n```\n")
78                 sys.exit(1)
79
80         except Exception as e:
81             sys.exit(
82                 f"Git repository is not clean. Clean it up before running ({e})")
83
84     def run(self):
85         assert self.branch
86         assert self.repo
87         self.check_clean()
88
89         try:
90             git_rename_limit = int(self.git("config", "merge.renameLimit"))
91         except subprocess.CalledProcessError:
92             git_rename_limit = 0
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)
98
99         try:
100             self.rebase()
101         finally:
102             if self.git_rename_limit is not None:
103                 self.git("config", "merge.renameLimit", str(self.git_rename_limit))
104
105     def rebase(self):
106         repo = urlparse(self.repo)
107
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",
114                 can_fail=True)
115         fprint(f"{green(' OK')}\n", nested=False)
116
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)
122
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)
128
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')
132         try:
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)
142             raise
143         if ret:
144             fprint(f"{green(' OK')}\n", nested=False)
145         else:
146             self.git("checkout", prevbranch)
147             self.git("branch", "-D", tmpbranchname)
148             fprint(f"{red(' ERROR')}\n", nested=False)
149
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)
155
156         for sha in reversed(shas.split()):
157             fprint(f' - Cherry picking: {bold(sha)}\n')
158             try:
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')
165                 return False
166         return True
167
168     def git(self, *args, can_fail=False, interaction_message=None, call=False, revert_operation=None):
169         retry = True
170         while retry:
171             retry = False
172             try:
173                 if not call:
174                     try:
175                         return subprocess.check_output(["git"] + list(args),
176                                                     stdin=subprocess.DEVNULL,
177                                                     stderr=subprocess.STDOUT).decode()
178                     except Exception as e:
179                         if not can_fail:
180                             fprint(f"\n\n{bold(red('ERROR'))}: `git {' '.join(args)}` failed" + "\n", nested=False)
181                         raise
182                 else:
183                     subprocess.call(["git"] + list(args))
184                     return "All good"
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()
190                     else:
191                         out = "????"
192                     fprint(f"\n```"
193                           f"\n{out}\n"
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)
201                     try:
202                         if os.name == 'nt':
203                             shell = os.environ.get(
204                                 "COMSPEC", r"C:\WINDOWS\system32\cmd.exe")
205                         else:
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:
211                             retry = True
212                             continue
213                         elif e.returncode == 2:
214                             if revert_operation:
215                                 self.git(*revert_operation, can_fail=True)
216                         raise
217                     except Exception as e:
218                         # Result of subshell does not really matter
219                         pass
220
221                     return "User fixed it"
222
223                 if can_fail:
224                     return "Failed but we do not care"
225
226                 raise e
227
228
229 def main():
230     picker = GstCherryPicker()
231     PARSER.parse_args(namespace=picker)
232     picker.run()
233
234
235 if __name__ == '__main__':
236     main()