Support main as default branch

R=apolito@google.com

Change-Id: Ic338c698b8eb8d2e04fc1ef23ae4b13cae08b80f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/2404701
Reviewed-by: Anthony Polito <apolito@google.com>
Commit-Queue: Josip Sokcevic <sokcevic@google.com>
diff --git a/gerrit_client.py b/gerrit_client.py
index 6b3e9de..f646c7d 100755
--- a/gerrit_client.py
+++ b/gerrit_client.py
@@ -38,6 +38,32 @@
 
 
 @subcommand.usage('[args ...]')
+def CMDmovechanges(parser, args):
+  parser.add_option('-p', '--param', dest='params', action='append',
+                    help='repeatable query parameter, format: -p key=value')
+  parser.add_option('--destination_branch', dest='destination_branch',
+                    help='where to move changes to')
+
+  (opt, args) = parser.parse_args(args)
+  assert opt.destination_branch, "--destination_branch not defined"
+  host = urlparse.urlparse(opt.host).netloc
+
+  limit = 100
+  while True:
+    result = gerrit_util.QueryChanges(
+        host,
+        list(tuple(p.split('=', 1)) for p in opt.params),
+        limit=limit,
+    )
+    for change in result:
+      gerrit_util.MoveChange(host, change['id'], opt.destination_branch)
+
+    if len(result) < limit:
+      break
+  logging.info("Done")
+
+
+@subcommand.usage('[args ...]')
 def CMDbranchinfo(parser, args):
   parser.add_option('--branch', dest='branch', help='branch name')
 
diff --git a/gerrit_util.py b/gerrit_util.py
index 61604a3..0077f5f 100644
--- a/gerrit_util.py
+++ b/gerrit_util.py
@@ -706,6 +706,15 @@
   return ReadHttpJsonResponse(conn)
 
 
+def MoveChange(host, change, destination_branch):
+  """Move a Gerrit change to different destination branch."""
+  path = 'changes/%s/move' % change
+  body = {'destination_branch': destination_branch}
+  conn = CreateHttpConn(host, path, reqtype='POST', body=body)
+  return ReadHttpJsonResponse(conn)
+
+
+
 def RestoreChange(host, change, msg=''):
   """Restores a previously abandoned change."""
   path = 'changes/%s/restore' % change
diff --git a/git_cl.py b/git_cl.py
index d6ab3ef..58cb54e 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -108,6 +108,9 @@
     'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
 }
 
+DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
+DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
+
 # Valid extensions for files we want to lint.
 DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
 DEFAULT_LINT_IGNORE_REGEX = r"$^"
@@ -641,7 +644,7 @@
   yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
   but this functionality appears to break when explicitly passing files to
   yapf for formatting. According to
-  https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
+  https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
   the .yapfignore file should be in the directory that yapf is invoked from,
   which we assume to be the top level directory in this case.
 
@@ -987,7 +990,7 @@
     self.more_cc.extend(more_cc)
 
   def GetBranch(self):
-    """Returns the short branch name, e.g. 'master'."""
+    """Returns the short branch name, e.g. 'main'."""
     if not self.branch:
       branchref = scm.GIT.GetBranchRef(settings.GetRoot())
       if not branchref:
@@ -997,7 +1000,7 @@
     return self.branch
 
   def GetBranchRef(self):
-    """Returns the full branch name, e.g. 'refs/heads/master'."""
+    """Returns the full branch name, e.g. 'refs/heads/main'."""
     self.GetBranch()  # Poke the lazy loader.
     return self.branchref
 
@@ -1016,7 +1019,7 @@
   @staticmethod
   def FetchUpstreamTuple(branch):
     """Returns a tuple containing remote and remote ref,
-       e.g. 'origin', 'refs/heads/master'
+       e.g. 'origin', 'refs/heads/main'
     """
     remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
         settings.GetRoot(), branch)
@@ -1024,7 +1027,7 @@
       DieWithError(
          'Unable to determine default branch to diff against.\n'
          'Either pass complete "git diff"-style arguments, like\n'
-         '  git cl upload origin/master\n'
+         '  git cl upload origin/main\n'
          'or verify this branch is set up to track another \n'
          '(via the --track argument to "git checkout -b ...").')
 
@@ -1226,8 +1229,8 @@
           ('\nFailed to diff against upstream branch %s\n\n'
            'This branch probably doesn\'t exist anymore. To reset the\n'
            'tracking branch, please run\n'
-           '    git branch --set-upstream-to origin/master %s\n'
-           'or replace origin/master with the relevant branch') %
+           '    git branch --set-upstream-to origin/main %s\n'
+           'or replace origin/main with the relevant branch') %
           (upstream, self.GetBranch()))
 
   def UpdateDescription(self, description, force=False):
@@ -3939,10 +3942,20 @@
     # Handle the refs that need to land in different refs.
     remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
 
+  # Migration to new default branch, only if available on remote.
+  allow_push_on_master = bool(os.environ.get("ALLOW_PUSH_TO_MASTER", None))
+  if remote_branch == DEFAULT_OLD_BRANCH and not allow_push_on_master:
+    if RunGit(['show-branch', DEFAULT_NEW_BRANCH], error_ok=True,
+              stderr=subprocess2.PIPE):
+      # TODO(crbug.com/ID): Print location to local git migration script.
+      print("WARNING: Using new branch name %s instead of %s" % (
+          DEFAULT_NEW_BRANCH, DEFAULT_OLD_BRANCH))
+      remote_branch = DEFAULT_NEW_BRANCH
+
   # Create the true path to the remote branch.
   # Does the following translation:
   # * refs/remotes/origin/refs/diff/test -> refs/diff/test
-  # * refs/remotes/origin/master -> refs/heads/master
+  # * refs/remotes/origin/main -> refs/heads/main
   # * refs/remotes/branch-heads/test -> refs/branch-heads/test
   if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
     remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
@@ -4026,7 +4039,7 @@
                     '--target-branch',
                     metavar='TARGET',
                     help='Apply CL to remote ref TARGET.  ' +
-                         'Default: remote branch head, or master')
+                         'Default: remote branch head, or main')
   parser.add_option('--squash', action='store_true',
                     help='Squash multiple commits into one')
   parser.add_option('--no-squash', action='store_false', dest='squash',
@@ -4382,7 +4395,7 @@
       '-r', '--revision',
       help='Revision to use for the tryjob; default: the revision will '
            'be determined by the try recipe that builder runs, which usually '
-           'defaults to HEAD of origin/master')
+           'defaults to HEAD of origin/master or origin/main')
   group.add_option(
       '-c', '--clobber', action='store_true', default=False,
       help='Force a clobber before building; that is don\'t do an '
diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py
index 45f34c8..cf788f7 100755
--- a/tests/git_cl_test.py
+++ b/tests/git_cl_test.py
@@ -647,7 +647,7 @@
   def _gerrit_base_calls(cls, issue=None, fetched_description=None,
                          fetched_status=None, other_cl_owner=None,
                          custom_cl_base=None, short_hostname='chromium',
-                         change_id=None):
+                         change_id=None, new_default=False):
     calls = []
     if custom_cl_base:
       ancestor_revision = custom_cl_base
@@ -655,8 +655,8 @@
       # Determine ancestor_revision to be merge base.
       ancestor_revision = 'fake_ancestor_sha'
       calls += [
-        (('get_or_create_merge_base', 'master', 'refs/remotes/origin/master'),
-         ancestor_revision),
+        (('get_or_create_merge_base', 'master',
+          'refs/remotes/origin/master'), ancestor_revision),
       ]
 
     if issue:
@@ -682,6 +682,10 @@
           [ancestor_revision, 'HEAD']),),
        '+dat'),
     ]
+    calls += [
+      ((['git', 'show-branch', 'refs/remotes/origin/main'], ),
+         '1' if new_default else callError(1)),
+    ]
 
     return calls
 
@@ -693,7 +697,9 @@
                            short_hostname='chromium',
                            labels=None, change_id=None,
                            final_description=None, gitcookies_exists=True,
-                           force=False, edit_description=None):
+                           force=False, edit_description=None,
+                           new_default=False):
+    default_branch = 'main' if new_default else 'master';
     if post_amend_description is None:
       post_amend_description = description
     cc = cc or []
@@ -722,14 +728,14 @@
 
       if custom_cl_base is None:
         calls += [
-          (('get_or_create_merge_base', 'master', 'refs/remotes/origin/master'),
-           'origin/master'),
+          (('get_or_create_merge_base', 'master',
+            'refs/remotes/origin/master'), 'origin/' + default_branch),
         ]
-        parent = 'origin/master'
+        parent = 'origin/' + default_branch
       else:
         calls += [
           ((['git', 'merge-base', '--is-ancestor', custom_cl_base,
-             'refs/remotes/origin/master'],),
+             'refs/remotes/origin/' + default_branch],),
            callError(1)),   # Means not ancenstor.
           (('ask_for_data',
             'Do you take responsibility for cleaning up potential mess '
@@ -748,7 +754,7 @@
       ]
     else:
       ref_to_push = 'HEAD'
-      parent = 'origin/refs/heads/master'
+      parent = 'origin/refs/heads/' + default_branch
 
     calls += [
       (('SaveDescriptionBackup',), None),
@@ -834,7 +840,7 @@
       (('time.time',), 1000,),
       ((['git', 'push',
          'https://%s.googlesource.com/my/repo' % short_hostname,
-         ref_to_push + ':refs/for/refs/heads/master' + ref_suffix],),
+         ref_to_push + ':refs/for/refs/heads/' + default_branch + ref_suffix],),
        (('remote:\n'
          'remote: Processing changes: (\)\n'
          'remote: Processing changes: (|)\n'
@@ -848,8 +854,8 @@
              ' XXX\n'
          'remote:\n'
          'To https://%s.googlesource.com/my/repo\n'
-         ' * [new branch]      hhhh -> refs/for/refs/heads/master\n'
-         ) % (short_hostname, short_hostname)),),
+         ' * [new branch]      hhhh -> refs/for/refs/heads/%s\n'
+         ) % (short_hostname, short_hostname, default_branch)),),
       (('time.time',), 2000,),
       (('add_repeated',
         'sub_commands',
@@ -990,7 +996,8 @@
       force=False,
       log_description=None,
       edit_description=None,
-      fetched_description=None):
+      fetched_description=None,
+      new_default=False):
     """Generic gerrit upload test framework."""
     if squash_mode is None:
       if '--no-squash' in upload_args:
@@ -1060,7 +1067,8 @@
         other_cl_owner=other_cl_owner,
         custom_cl_base=custom_cl_base,
         short_hostname=short_hostname,
-        change_id=change_id)
+        change_id=change_id,
+        new_default=new_default)
     if fetched_status != 'ABANDONED':
       mock.patch(
           'gclient_utils.temporary_file', TemporaryFileMock()).start()
@@ -1078,7 +1086,8 @@
           final_description=final_description,
           gitcookies_exists=gitcookies_exists,
           force=force,
-          edit_description=edit_description)
+          edit_description=edit_description,
+          new_default=new_default)
     # Uncomment when debugging.
     # print('\n'.join(map(lambda x: '%2i: %s' % x, enumerate(self.calls))))
     git_cl.main(['upload'] + upload_args)
@@ -1444,6 +1453,10 @@
     self.assertEqual(expected, actual)
 
   def test_get_hash_tags(self):
+    self.calls = [
+        ((['git', 'show-branch', 'refs/remotes/origin/main'], ),
+            callError(1)),
+    ] * 9
     cases = [
       ('', []),
       ('a', []),
@@ -2659,6 +2672,16 @@
     cl = git_cl.Changelist(issue=123456)
     self.assertEqual(cl._GerritChangeIdentifier(), '123456')
 
+  def test_gerrit_new_default(self):
+    self._run_gerrit_upload_test(
+        [],
+        'desc ✔\n\nBUG=\n\nChange-Id: I123456789\n',
+        [],
+        squash=False,
+        squash_mode='override_nosquash',
+        change_id='I123456789',
+        new_default=True)
+
 
 class ChangelistTest(unittest.TestCase):
   def setUp(self):