scripts: Add a script to rebase branches from old modules into monorepo
[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.")
23 PARSER.add_argument("branch", help="The branch to rebase.")
24
25 log_depth = []               # type: T.List[str]
26
27 @contextmanager
28 def nested(name=''):
29     global log_depth
30     log_depth.append(name)
31     try:
32         yield
33     finally:
34         log_depth.pop()
35
36 def bold(text: str):
37     return f"\033[1m{text}\033[0m"
38
39 def green(text: str):
40     return f"\033[1;32m{text}\033[0m"
41
42 def red(text: str):
43     return f"\033[1;31m{text}\033[0m"
44
45 def yellow(text: str):
46     return f"\033[1;33m{text}\033[0m"
47
48 def fprint(msg, nested=True):
49     if log_depth:
50         prepend = log_depth[-1] + ' | ' if nested else ''
51     else:
52         prepend = ''
53
54     print(prepend + msg, end="")
55     sys.stdout.flush()
56
57
58 class GstCherryPicker:
59     def __init__(self):
60
61         self.branch = None
62         self.repo = None
63         self.module = None
64
65         self.git_rename_limit = None
66
67     def check_clean(self):
68         try:
69             out = self.git("status", "--porcelain")
70             if out:
71                 fprint("\n" + red('Git repository is not clean:') + "\n```\n" + out + "\n```\n")
72                 sys.exit(1)
73
74         except Exception as e:
75             sys.exit(
76                 f"Git repository is not clean. Clean it up before running ({e})")
77
78     def run(self):
79         assert self.branch
80         assert self.repo
81         self.check_clean()
82
83         try:
84             git_rename_limit = int(self.git("config", "merge.renameLimit"))
85         except subprocess.CalledProcessError:
86             git_rename_limit = 0
87         if int(git_rename_limit) < 999999:
88             self.git_rename_limit = git_rename_limit
89             fprint("-> Setting git rename limit to 999999 so we can properly cherry-pick between repos")
90             self.git("config", "merge.renameLimit", "999999")
91             fprint(f"{green(' OK')}\n", nested=False)
92
93         try:
94             self.rebase()
95         finally:
96             if self.git_rename_limit is not None:
97                 self.git("config", "merge.renameLimit", str(self.git_rename_limit))
98
99     def rebase(self):
100         repo = urlparse(self.repo)
101
102         repo_path = P(repo.path)
103         self.module = module = repo_path.stem
104         remote_name = f"{module}-{repo_path.parent.name}"
105         fprint('Adding remotes...')
106         self.git("remote", "add", remote_name, self.repo, can_fail=True)
107         self.git("remote", "add", module, f"{URL}gstreamer/{module}.git",
108                 can_fail=True)
109         fprint(f"{green(' OK')}\n", nested=False)
110
111         fprint(f'Fetching {remote_name}...')
112         self.git("fetch", remote_name,
113             interaction_message=f"fetching {remote_name} with:\n"
114             f"   `$ git fetch {remote_name}`")
115         fprint(f"{green(' OK')}\n", nested=False)
116
117         fprint(f'Fetching {module}...')
118         self.git("fetch", module,
119             interaction_message=f"fetching {module} with:\n"
120             f"   `$ git fetch {module}`")
121         fprint(f"{green(' OK')}\n", nested=False)
122
123         prevbranch = self.git("rev-parse", "--abbrev-ref", "HEAD").strip()
124         tmpbranchname = f"{remote_name}_{self.branch}"
125         fprint(f'Checking out branch {remote_name}/{self.branch} as {tmpbranchname}\n')
126         try:
127             self.git("checkout", f"{remote_name}/{self.branch}", "-b", tmpbranchname)
128             self.git("rebase", f"{module}/master",
129                 interaction_message=f"Failed rebasing {remote_name}/{self.branch} on {module}/master with:\n"
130                 f"   `$ git rebase {module}/master`")
131             self.cherry_pick(tmpbranchname)
132         except:
133             self.git("rebase", "--abort", can_fail=True)
134             self.git("checkout", prevbranch)
135             self.git("branch", "-D", tmpbranchname)
136             raise
137         fprint(f"{green(' OK')}\n", nested=False)
138
139     def cherry_pick(self, branch):
140         shas = self.git('log', '--format=format:%H', f'{self.module}/master..').strip()
141         fprint(f'Resetting on origin/main')
142         self.git("reset", "--hard", "origin/main")
143         fprint(f"{green(' OK')}\n", nested=False)
144
145         for sha in reversed(shas.split()):
146             fprint(f' - Cherry picking: {bold(sha)}\n')
147             self.git("cherry-pick", sha,
148                      interaction_message=f"cherry-picking {sha} onto {branch} with:\n  "
149                                         f" `$ git cherry-pick {sha}`"
150             )
151
152
153     def git(self, *args, can_fail=False, interaction_message=None, call=False):
154         retry = True
155         while retry:
156             retry = False
157             try:
158                 if not call:
159                     try:
160                         return subprocess.check_output(["git"] + list(args),
161                                                     stdin=subprocess.DEVNULL,
162                                                     stderr=subprocess.STDOUT).decode()
163                     except:
164                         if not can_fail:
165                             fprint(f"\n\n{bold(red('ERROR'))}: `git {' '.join(args)}` failed" + "\n", nested=False)
166                         raise
167                 else:
168                     subprocess.call(["git"] + list(args))
169                     return "All good"
170             except Exception as e:
171                 if interaction_message:
172                     output = getattr(e, "output", b"")
173                     if output is not None:
174                         out = output.decode()
175                     else:
176                         out = "????"
177                     fprint(f"\n```"
178                           f"\n{out}\n"
179                           f"Entering a shell to fix:\n\n"
180                           f" {bold(interaction_message)}\n\n"
181                           f"You should then exit with the following codes:\n\n"
182                           f"  - {bold('`exit 0`')}: once you have fixed the problem and we can keep moving the \n"
183                           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"
184                           f"  - {bold('`exit 3`')}: stop the script and abandon moving your MRs\n"
185                           "\n```\n", nested=False)
186                     try:
187                         if os.name == 'nt':
188                             shell = os.environ.get(
189                                 "COMSPEC", r"C:\WINDOWS\system32\cmd.exe")
190                         else:
191                             shell = os.environ.get(
192                                 "SHELL", os.path.realpath("/bin/sh"))
193                         subprocess.check_call(shell)
194                     except subprocess.CalledProcessError as e:
195                         if e.returncode == 1:
196                             retry = True
197                             continue
198                         elif e.returncode == 3:
199                             sys.exit(3)
200                     except:
201                         # Result of subshell does not really matter
202                         pass
203
204                     return "User fixed it"
205
206                 if can_fail:
207                     return "Failed but we do not care"
208
209                 raise e
210
211
212 def main():
213     picker = GstCherryPicker()
214     PARSER.parse_args(namespace=picker)
215     picker.run()
216
217
218 if __name__ == '__main__':
219     main()
220