pngenc: mark output frames as I-frames
[platform/upstream/gstreamer.git] / scripts / move_mrs_to_monorepo.py
1 #!/usr/bin/env python3
2
3 from urllib.parse import urlparse
4 from contextlib import contextmanager
5 import os
6 import re
7 import sys
8 try:
9     import gitlab
10 except ModuleNotFoundError:
11     print("========================================================================", file=sys.stderr)
12     print("ERROR: Install python-gitlab with `python3 -m pip install python-gitlab python-dateutil`", file=sys.stderr)
13     print("========================================================================", file=sys.stderr)
14     sys.exit(1)
15
16 try:
17     from dateutil import parser as dateparse
18 except ModuleNotFoundError:
19     print("========================================================================", file=sys.stderr)
20     print("ERROR: Install dateutil with `python3 -m pip install dateutil`", file=sys.stderr)
21     print("========================================================================", file=sys.stderr)
22     sys.exit(1)
23 import argparse
24 import requests
25
26 import subprocess
27
28 ROOT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
29
30 URL = "https://gitlab.freedesktop.org/"
31 SIGN_IN_URL = URL + 'sign_in'
32 LOGIN_URL = URL + 'users/sign_in'
33 LOGIN_URL_LDAP = URL + '/users/auth/ldapmain/callback'
34
35 MONOREPO_REMOTE_NAME = 'origin'
36 NAMESPACE = "gstreamer"
37 MONOREPO_NAME = 'gstreamer'
38 MONOREPO_REMOTE = URL + f'{NAMESPACE}/{MONOREPO_NAME}'
39 MONOREPO_BRANCH = 'main'
40 PING_SIGN = '@'
41 MOVING_NAMESPACE = NAMESPACE
42
43 PARSER = argparse.ArgumentParser(
44     description="Move merge request from old GStreamer module to the new"
45                 "GStreamer 'monorepo'.\n"
46                 " All your pending merge requests from all GStreamer modules will"
47                 " be moved the the mono repository."
48 )
49 PARSER.add_argument("--skip-branch", action="store", nargs="*",
50                     help="Ignore MRs for branches which match those names.", dest="skipped_branches")
51 PARSER.add_argument("--skip-on-failure", action="store_true", default=False)
52 PARSER.add_argument("--dry-run", "-n", action="store_true", default=False)
53 PARSER.add_argument("--use-branch-if-exists",
54                     action="store_true", default=False)
55 PARSER.add_argument("--list-mrs-only", action="store_true", default=False)
56 PARSER.add_argument(
57     "-c",
58     "--config-file",
59     action="append",
60     dest='config_files',
61     help="Configuration file to use. Can be used multiple times.",
62     required=False,
63 )
64 PARSER.add_argument(
65     "-g",
66     "--gitlab",
67     help=(
68         "Which configuration section should "
69         "be used. If not defined, the default selection "
70         "will be used."
71     ),
72     required=False,
73 )
74 PARSER.add_argument(
75     "-m",
76     "--module",
77     help="GStreamer module to move MRs for. All if none specified. Can be used multiple times.",
78     dest='modules',
79     action="append",
80     required=False,
81 )
82 PARSER.add_argument(
83     "-mr",
84     "--mr-url",
85     default=None,
86     type=str,
87     help=(
88         "URL of the MR to work on."
89     ),
90     required=False,
91 )
92
93 GST_PROJECTS = [
94     'gstreamer',
95     'gst-plugins-base',
96     'gst-plugins-good',
97     'gst-plugins-bad',
98     'gst-plugins-ugly',
99     'gst-libav',
100     'gst-rtsp-server',
101     'gstreamer-vaapi',
102     'gstreamer-sharp',
103     'gst-python',
104     'gst-omx',
105     'gst-editing-services',
106     'gst-devtools',
107     'gst-docs',
108     'gst-examples',
109     'gst-build',
110     'gst-ci',
111 ]
112
113 GST_PROJECTS_ID = {
114     'gstreamer': 1357,
115     'gst-rtsp-server': 1362,
116     'gstreamer-vaapi': 1359,
117     'gstreamer-sharp': 1358,
118     'gst-python': 1355,
119     'gst-plugins-ugly': 1354,
120     'gst-plugins-good': 1353,
121     'gst-plugins-base': 1352,
122     'gst-plugins-bad': 1351,
123     'gst-omx': 1350,
124     'gst-libav': 1349,
125     'gst-integration-testsuites': 1348,
126     'gst-examples': 1347,
127     'gst-editing-services': 1346,
128     'gst-docs': 1345,
129     'gst-devtools': 1344,
130     'gst-ci': 1343,
131     'gst-build': 1342,
132 }
133
134 # We do not want to deal with LFS
135 os.environ["GIT_LFS_SKIP_SMUDGE"] = "1"
136
137
138 log_depth = []               # type: T.List[str]
139
140
141 @contextmanager
142 def nested(name=''):
143     global log_depth
144     log_depth.append(name)
145     try:
146         yield
147     finally:
148         log_depth.pop()
149
150
151 def bold(text: str):
152     return f"\033[1m{text}\033[0m"
153
154
155 def green(text: str):
156     return f"\033[1;32m{text}\033[0m"
157
158
159 def red(text: str):
160     return f"\033[1;31m{text}\033[0m"
161
162
163 def yellow(text: str):
164     return f"\033[1;33m{text}\033[0m"
165
166
167 def fprint(msg, nested=True):
168     if log_depth:
169         prepend = log_depth[-1] + ' | ' if nested else ''
170     else:
171         prepend = ''
172
173     print(prepend + msg, end="")
174     sys.stdout.flush()
175
176
177 class GstMRMover:
178     def __init__(self):
179
180         self.modules = []
181         self.gitlab = None
182         self.config_files = []
183         self.gl = None
184         self.mr = None
185         self.mr_url = None
186         self.all_projects = []
187         self.skipped_branches = []
188         self.git_rename_limit = None
189         self.skip_on_failure = None
190         self.dry_run = False
191
192     def connect(self):
193         fprint("Logging into gitlab...")
194
195         if self.gitlab:
196             gl = gitlab.Gitlab.from_config(self.gitlab, self.config_files)
197             fprint(f"{green(' OK')}\n", nested=False)
198             return gl
199
200         gitlab_api_token = os.environ.get('GITLAB_API_TOKEN')
201         if gitlab_api_token:
202             gl = gitlab.Gitlab(URL, private_token=gitlab_api_token)
203             fprint(f"{green(' OK')}\n", nested=False)
204             return gl
205
206         session = requests.Session()
207         sign_in_page = session.get(SIGN_IN_URL).content.decode()
208         for line in sign_in_page.split('\n'):
209             m = re.search('name="authenticity_token" value="([^"]+)"', line)
210             if m:
211                 break
212
213         token = None
214         if m:
215             token = m.group(1)
216
217         if not token:
218             fprint(f"{red('Unable to find the authenticity token')}\n")
219             sys.exit(1)
220
221         for data, url in [
222             ({'user[login]': 'login_or_email',
223               'user[password]': 'SECRET',
224               'authenticity_token': token}, LOGIN_URL),
225             ({'username': 'login_or_email',
226               'password': 'SECRET',
227               'authenticity_token': token}, LOGIN_URL_LDAP)]:
228
229             r = session.post(url, data=data)
230             if r.status_code != 200:
231                 continue
232
233             try:
234                 gl = gitlab.Gitlab(URL, api_version=4, session=session)
235                 gl.auth()
236             except gitlab.exceptions.GitlabAuthenticationError as e:
237                 continue
238             return gl
239
240         sys.exit(bold(f"{red('FAILED')}.\n\nPlease go to:\n\n"
241                       '   https://gitlab.freedesktop.org/-/profile/personal_access_tokens\n\n'
242                       f'and generate a token {bold("with read/write access to all but the registry")},'
243                       ' then set it in the "GITLAB_API_TOKEN" environment variable:"'
244                       f'\n\n  $ GITLAB_API_TOKEN=<your token> {" ".join(sys.argv)}\n'))
245
246     def git(self, *args, can_fail=False, interaction_message=None, call=False, revert_operation=None):
247         cwd = ROOT_DIR
248         retry = True
249         while retry:
250             retry = False
251             try:
252                 if not call:
253                     try:
254                         return subprocess.check_output(["git"] + list(args), cwd=cwd,
255                                                        stdin=subprocess.DEVNULL,
256                                                        stderr=subprocess.STDOUT).decode()
257                     except subprocess.CalledProcessError:
258                         if not can_fail:
259                             fprint(
260                                 f"\n\n{bold(red('ERROR'))}: `git {' '.join(args)}` failed" + "\n", nested=False)
261                         raise
262                 else:
263                     subprocess.call(["git"] + list(args), cwd=cwd)
264                     return "All good"
265             except Exception as e:
266                 if interaction_message:
267                     if self.skip_on_failure:
268                         return "SKIP"
269                     output = getattr(e, "output", b"")
270                     if output is not None:
271                         out = output.decode()
272                     else:
273                         out = "????"
274                     fprint(f"\n```"
275                            f"\n{out}\n"
276                            f"Entering a shell in {cwd} to fix:\n\n"
277                            f" {bold(interaction_message)}\n\n"
278                            f"You should then exit with the following codes:\n\n"
279                            f"  - {bold('`exit 0`')}: once you have fixed the problem and we can keep moving the merge request\n"
280                            f"  - {bold('`exit 1`')}: {bold('retry')}: once you have let the repo in a state where the operation should be to retried\n"
281                            f"  - {bold('`exit 2`')}: to skip that merge request\n"
282                            f"  - {bold('`exit 3`')}: stop the script and abandon moving your MRs\n"
283                            "\n```\n", nested=False)
284                     try:
285                         if os.name == 'nt':
286                             shell = os.environ.get(
287                                 "COMSPEC", r"C:\WINDOWS\system32\cmd.exe")
288                         else:
289                             shell = os.environ.get(
290                                 "SHELL", os.path.realpath("/bin/sh"))
291                         subprocess.check_call(shell, cwd=cwd)
292                     except subprocess.CalledProcessError as e:
293                         if e.returncode == 1:
294                             retry = True
295                             continue
296                         elif e.returncode == 2:
297                             if revert_operation:
298                                 self.git(*revert_operation, can_fail=True)
299                             return "SKIP"
300                         elif e.returncode == 3:
301                             if revert_operation:
302                                 self.git(*revert_operation, can_fail=True)
303                             sys.exit(3)
304                     except Exception:
305                         # Result of subshell does not really matter
306                         pass
307
308                     return "User fixed it"
309
310                 if can_fail:
311                     return "Failed but we do not care"
312
313                 raise e
314
315     def cleanup_args(self):
316         if self.mr_url:
317             self.modules.append(GST_PROJECTS[0])
318             (namespace, module, _, _, mr) = os.path.normpath(urlparse(self.mr_url).path).split('/')[1:]
319             self.modules.append(module)
320             self.mr = int(mr)
321         elif not self.modules:
322             if self.mr:
323                 sys.exit(f"{red(f'Merge request #{self.mr} specified without module')}\n\n"
324                          f"{bold(' -> Use `--module` to specify which module the MR is from.')}")
325
326             self.modules = GST_PROJECTS
327         else:
328             VALID_PROJECTS = GST_PROJECTS[1:]
329             for m in self.modules:
330                 if m not in VALID_PROJECTS:
331                     projects = '\n- '.join(VALID_PROJECTS)
332                     sys.exit(
333                         f"{red(f'Unknown module {m}')}\nModules are:\n- {projects}")
334             if self.mr and len(self.modules) > 1:
335                 sys.exit(f"{red(f'Merge request #{self.mr} specified but several modules where specified')}\n\n"
336                          f"{bold(' -> Use `--module` only once to specify an merge request.')}")
337             self.modules.append(GST_PROJECTS[0])
338
339     def run(self):
340         self.cleanup_args()
341         self.gl = self.connect()
342         self.gl.auth()
343
344         # Skip pre-commit hooks when migrating. Some users may have a
345         # different version of gnu indent and that can lead to cherry-pick
346         # failing.
347         os.environ["GST_DISABLE_PRE_COMMIT_HOOKS"] = "1"
348
349         try:
350             prevbranch = self.git(
351                 "rev-parse", "--abbrev-ref", "HEAD", can_fail=True).strip()
352         except Exception:
353             fprint(bold(yellow("Not on a branch?\n")), indent=False)
354             prevbranch = None
355
356         try:
357             self.setup_repo()
358
359             from_projects, to_project = self.fetch_projects()
360
361             with nested('  '):
362                 self.move_mrs(from_projects, to_project)
363         finally:
364             if self.git_rename_limit is not None:
365                 self.git("config", "merge.renameLimit",
366                          str(self.git_rename_limit))
367             if prevbranch:
368                 fprint(f'Back to {prevbranch}\n')
369                 self.git("checkout", prevbranch)
370
371     def fetch_projects(self):
372         fprint("Fetching projects... ")
373         self.all_projects = [proj for proj in self.gl.projects.list(
374             membership=1, all=True) if proj.name in self.modules]
375
376         try:
377             self.user_project, = [p for p in self.all_projects
378                                   if p.namespace['path'] == self.gl.user.username
379                                   and p.name == MONOREPO_NAME]
380         except ValueError:
381             fprint(
382                 f"{red(f'ERROR')}\n\nCould not find repository {self.gl.user.name}/{MONOREPO_NAME}")
383             fprint(f"{red(f'Got to https://gitlab.freedesktop.org/gstreamer/gstreamer/ and create a fork so we can move your Merge requests.')}")
384             sys.exit(1)
385         fprint(f"{green(' OK')}\n", nested=False)
386
387         from_projects = []
388         user_projects_name = [proj.name for proj in self.all_projects if proj.namespace['path']
389                               == self.gl.user.username and proj.name in GST_PROJECTS]
390         for project, id in GST_PROJECTS_ID.items():
391             if project not in user_projects_name or project == 'gstreamer':
392                 continue
393
394             projects = [p for p in self.all_projects if p.id == id]
395             if not projects:
396                 upstream_project = self.gl.projects.get(id)
397             else:
398                 upstream_project, = projects
399             assert project
400
401             from_projects.append(upstream_project)
402
403         fprint(f"\nMoving MRs from:\n")
404         fprint(f"----------------\n")
405         for p in from_projects:
406             fprint(f"  - {bold(p.path_with_namespace)}\n")
407
408         to_project = self.gl.projects.get(GST_PROJECTS_ID['gstreamer'])
409         fprint(f"To: {bold(to_project.path_with_namespace)}\n\n")
410
411         return from_projects, to_project
412
413     def recreate_mr(self, project, to_project, mr):
414         branch = f"{project.name}-{mr.source_branch}"
415         if not self.create_branch_for_mr(branch, project, mr):
416             return None
417
418         description = f"**Copied from {URL}/{project.path_with_namespace}/-/merge_requests/{mr.iid}**\n\n{mr.description}"
419
420         title = mr.title
421         if ':' not in mr.title:
422             title = f"{project.name}: {mr.title}"
423
424         new_mr_dict = {
425             'source_branch': branch,
426             'allow_collaboration': True,
427             'remove_source_branch': True,
428             'target_project_id': to_project.id,
429             'target_branch': MONOREPO_BRANCH,
430             'title': title,
431             'labels': mr.labels,
432             'description': description,
433         }
434
435         try:
436             fprint(f"-> Recreating MR '{bold(mr.title)}'...")
437             if self.dry_run:
438                 fprint(f"\nDry info:\n{new_mr_dict}\n")
439             else:
440                 new_mr = self.user_project.mergerequests.create(new_mr_dict)
441                 fprint(f"{green(' OK')}\n", nested=False)
442         except gitlab.exceptions.GitlabCreateError as e:
443             fprint(f"{yellow('SKIPPED')} (An MR already exists)\n", nested=False)
444             return None
445
446         fprint(f"-> Adding discussings from MR '{mr.title}'...")
447         if self.dry_run:
448             fprint(f"{green(' OK')}\n", nested=False)
449             return None
450
451         new_mr_url = f"{URL}/{to_project.path_with_namespace}/-/merge_requests/{new_mr.iid}"
452         for issue in mr.closes_issues():
453             obj = {'body': f'Fixing MR moved to: {new_mr_url}'}
454             issue.discussions.create(obj)
455
456         mr_url = f"{URL}/{project.path_with_namespace}/-/merge_requests/{mr.iid}"
457         for discussion in mr.discussions.list():
458             # FIXME notes = [n for n in discussion.attributes['notes'] if n['type'] is not None]
459             notes = [n for n in discussion.attributes['notes']]
460             if not notes:
461                 continue
462
463             new_discussion = None
464             for note in notes:
465                 note = discussion.notes.get(note['id'])
466
467                 note_url = f"{mr_url}#note_{note.id}"
468                 when = dateparse.parse(
469                     note.created_at).strftime('on %d, %b %Y')
470                 body = f"**{note.author['name']} - {PING_SIGN}{note.author['username']} wrote [here]({note_url})** {when}:\n\n"
471                 body += '\n'.join([line for line in note.body.split('\n')])
472
473                 obj = {
474                     'body': body,
475                     'type': note.type,
476                     'resolvable': note.resolvable,
477                 }
478
479                 if new_discussion:
480                     new_discussion.notes.create(obj)
481                 else:
482                     new_discussion = new_mr.discussions.create(obj)
483
484                 if not note.resolvable or note.resolved:
485                     new_discussion.resolved = True
486                     new_discussion.save()
487
488         fprint(f"{green(' OK')}\n", nested=False)
489
490         print(f"New MR available at: {bold(new_mr_url)}\n")
491
492         return new_mr
493
494     def push_branch(self, branch):
495         fprint(
496             f"-> Pushing branch {branch} to remote {self.gl.user.username}...")
497         if self.git("push", "--no-verify", self.gl.user.username, branch,
498                     interaction_message=f"pushing {branch} to {self.gl.user.username} with:\n  "
499                     f" `$git push {self.gl.user.username} {branch}`") == "SKIP":
500             fprint(yellow("'SKIPPED' (couldn't push)"), nested=False)
501
502             return False
503
504         fprint(f"{green(' OK')}\n", nested=False)
505
506         return True
507
508     def create_branch_for_mr(self, branch, project, mr):
509         remote_name = project.name + '-' + self.gl.user.username
510         remote_branch = f"{MONOREPO_REMOTE_NAME}/{MONOREPO_BRANCH}"
511         if self.use_branch_if_exists:
512             try:
513                 self.git("checkout", branch)
514                 self.git("show", remote_branch + "..", call=True)
515                 if self.dry_run:
516                     fprint("Dry run... not creating MR")
517                     return True
518                 cont = input('\n     Create MR [y/n]? ')
519                 if cont.strip().lower() != 'y':
520                     fprint("Cancelled")
521                     return False
522                 return self.push_branch(branch)
523             except subprocess.CalledProcessError as e:
524                 pass
525
526         self.git("remote", "add", remote_name,
527                  f"{URL}{self.gl.user.username}/{project.name}.git", can_fail=True)
528         self.git("fetch", remote_name)
529
530         if self.git("checkout", remote_branch, "-b", branch,
531                     interaction_message=f"checking out branch with `git checkout {remote_branch} -b {branch}`") == "SKIP":
532             fprint(
533                 bold(f"{red('SKIPPED')} (couldn't checkout)\n"), nested=False)
534             return False
535
536         # unset upstream to avoid to push to main (ie push.default = tracking)
537         self.git("branch", branch, "--unset-upstream")
538
539         for commit in reversed([c for c in mr.commits()]):
540             if self.git("cherry-pick", commit.id,
541                         interaction_message=f"cherry-picking {commit.id} onto {branch} with:\n  "
542                         f" `$ git cherry-pick {commit.id}`",
543                         revert_operation=["cherry-pick", "--abort"]) == "SKIP":
544                 fprint(
545                     f"{yellow('SKIPPED')} (couldn't cherry-pick).", nested=False)
546                 return False
547
548         self.git("show", remote_branch + "..", call=True)
549         if self.dry_run:
550             fprint("Dry run... not creating MR\n")
551             return True
552         cont = input('\n     Create MR [y/n]? ')
553         if cont.strip().lower() != 'y':
554             fprint(f"{red('Cancelled')}\n", nested=False)
555             return False
556
557         return self.push_branch(branch)
558
559     def move_mrs(self, from_projects, to_project):
560         failed_mrs = []
561         found_mr = None
562         for from_project in from_projects:
563             with nested(f'{bold(from_project.path_with_namespace)}'):
564                 fprint(f'Fetching mrs')
565                 mrs = [mr for mr in from_project.mergerequests.list(
566                     all=True, author_id=self.gl.user.id) if mr.author['username'] == self.gl.user.username and mr.state == "opened"]
567                 if not mrs:
568                     fprint(f"{yellow(' None')}\n", nested=False)
569                     continue
570
571                 fprint(f"{green(' DONE')}\n", nested=False)
572
573                 for mr in mrs:
574                     if self.mr:
575                         if self.mr != mr.iid:
576                             continue
577                         found_mr = True
578                     fprint(
579                         f'Moving {mr.source_branch} "{mr.title}": {URL}{from_project.path_with_namespace}/merge_requests/{mr.iid}... ')
580                     if mr.source_branch in self.skipped_branches:
581                         print(f"{yellow('SKIPPED')} (blacklisted branch)")
582                         failed_mrs.append(
583                             f"{URL}{from_project.path_with_namespace}/merge_requests/{mr.iid}")
584                         continue
585                     if self.list_mrs_only:
586                         fprint("\n"f"List only: {yellow('SKIPPED')}\n")
587                         continue
588
589                     with nested(f'{bold(from_project.path_with_namespace)}: {mr.iid}'):
590                         new_mr = self.recreate_mr(from_project, to_project, mr)
591                         if not new_mr:
592                             if not self.dry_run:
593                                 failed_mrs.append(
594                                     f"{URL}{from_project.path_with_namespace}/merge_requests/{mr.iid}")
595                         else:
596                             fprint(f"{green(' OK')}\n", nested=False)
597
598                         self.close_mr(from_project, to_project, mr, new_mr)
599
600             fprint(
601                 f"\n{yellow('DONE')} with {from_project.path_with_namespace}\n\n", nested=False)
602
603         if self.mr and not found_mr:
604             sys.exit(
605                 bold(red(f"\n==> Couldn't find MR {self.mr} in {self.modules[0]}\n")))
606
607         for mr in failed_mrs:
608             fprint(f"Didn't move MR: {mr}\n")
609
610     def close_mr(self, project, to_project, mr, new_mr):
611         if new_mr:
612             new_mr_url = f"{URL}/{to_project.path_with_namespace}/-/merge_requests/{new_mr.iid}"
613         else:
614             new_mr_url = None
615         mr_url = f"{URL}/{project.path_with_namespace}/-/merge_requests/{mr.iid}"
616         cont = input(f'\n  Close old MR {mr_url} "{bold(mr.title)}" ? [y/n]')
617         if cont.strip().lower() != 'y':
618             fprint(f"{yellow('Not closing old MR')}\n")
619         else:
620             obj = None
621             if new_mr_url:
622                 obj = {'body': f"Moved to: {new_mr_url}"}
623             else:
624                 ret = input(
625                     f"Write a comment to add while closing MR {mr.iid} '{bold(mr.title)}':\n\n").strip()
626                 if ret:
627                     obj = {'body': ret}
628
629             if self.dry_run:
630                 fprint(f"{bold('Dry run, not closing')}\n", nested=False)
631             else:
632                 if obj:
633                     mr.discussions.create(obj)
634                 mr.state_event = 'close'
635                 mr.save()
636                 fprint(
637                     f'Old MR {mr_url} "{bold(mr.title)}" {yellow("CLOSED")}\n')
638
639     def setup_repo(self):
640         fprint(f"Setting up '{bold(ROOT_DIR)}'...")
641
642         try:
643             out = self.git("status", "--porcelain")
644             if out:
645                 fprint("\n" + red('Git repository is not clean:')
646                        + "\n```\n" + out + "\n```\n")
647                 sys.exit(1)
648
649         except Exception as e:
650             exit(
651                 f"Git repository{ROOT_DIR} is not clean. Clean it up before running {sys.argv[0]}\n ({e})")
652
653         self.git('remote', 'add', MONOREPO_REMOTE_NAME,
654                  MONOREPO_REMOTE, can_fail=True)
655         self.git('fetch', MONOREPO_REMOTE_NAME)
656
657         self.git('remote', 'add', self.gl.user.username,
658                  f"git@gitlab.freedesktop.org:{self.gl.user.username}/gstreamer.git", can_fail=True)
659         self.git('fetch', self.gl.user.username,
660                  interaction_message=f"Setup your fork of {URL}gstreamer/gstreamer as remote called {self.gl.user.username}")
661         fprint(f"{green(' OK')}\n", nested=False)
662
663         try:
664             git_rename_limit = int(self.git("config", "merge.renameLimit"))
665         except subprocess.CalledProcessError:
666             git_rename_limit = 0
667         if int(git_rename_limit) < 999999:
668             self.git_rename_limit = git_rename_limit
669             fprint(
670                 "-> Setting git rename limit to 999999 so we can properly cherry-pick between repos\n")
671             self.git("config", "merge.renameLimit", "999999")
672
673
674 def main():
675     mover = GstMRMover()
676     PARSER.parse_args(namespace=mover)
677     mover.run()
678
679
680 if __name__ == '__main__':
681     main()