[depot_tools] New "git cl upload" flag to traverse dependent branches and re-upload them.
Motivation:
The conversation in https://docs.google.com/document/d/1KZGFKZpOPvco81sYVRCzwlnjGctup71RAzY0MSb0ntc/edit?disco=AAAAAXU60E8
BUG=502257
Review URL: https://codereview.chromium.org/1191473002
git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@295779 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/git_cl.py b/git_cl.py
index 50ba343..288e281 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -10,6 +10,7 @@
from distutils.version import LooseVersion
from multiprocessing.pool import ThreadPool
import base64
+import collections
import glob
import httplib
import json
@@ -1516,6 +1517,105 @@
url = cl.GetIssueURL()
yield (b, url, 'waiting' if url else 'error')
+
+def upload_branch_deps(cl, args):
+ """Uploads CLs of local branches that are dependents of the current branch.
+
+ If the local branch dependency tree looks like:
+ test1 -> test2.1 -> test3.1
+ -> test3.2
+ -> test2.2 -> test3.3
+
+ and you run "git cl upload --dependencies" from test1 then "git cl upload" is
+ run on the dependent branches in this order:
+ test2.1, test3.1, test3.2, test2.2, test3.3
+
+ Note: This function does not rebase your local dependent branches. Use it when
+ you make a change to the parent branch that will not conflict with its
+ dependent branches, and you would like their dependencies updated in
+ Rietveld.
+ """
+ if git_common.is_dirty_git_tree('upload-branch-deps'):
+ return 1
+
+ root_branch = cl.GetBranch()
+ if root_branch is None:
+ DieWithError('Can\'t find dependent branches from detached HEAD state. '
+ 'Get on a branch!')
+ if not cl.GetIssue() or not cl.GetPatchset():
+ DieWithError('Current branch does not have an uploaded CL. We cannot set '
+ 'patchset dependencies without an uploaded CL.')
+
+ branches = RunGit(['for-each-ref',
+ '--format=%(refname:short) %(upstream:short)',
+ 'refs/heads'])
+ if not branches:
+ print('No local branches found.')
+ return 0
+
+ # Create a dictionary of all local branches to the branches that are dependent
+ # on it.
+ tracked_to_dependents = collections.defaultdict(list)
+ for b in branches.splitlines():
+ tokens = b.split()
+ if len(tokens) == 2:
+ branch_name, tracked = tokens
+ tracked_to_dependents[tracked].append(branch_name)
+
+ print
+ print 'The dependent local branches of %s are:' % root_branch
+ dependents = []
+ def traverse_dependents_preorder(branch, padding=''):
+ dependents_to_process = tracked_to_dependents.get(branch, [])
+ padding += ' '
+ for dependent in dependents_to_process:
+ print '%s%s' % (padding, dependent)
+ dependents.append(dependent)
+ traverse_dependents_preorder(dependent, padding)
+ traverse_dependents_preorder(root_branch)
+ print
+
+ if not dependents:
+ print 'There are no dependent local branches for %s' % root_branch
+ return 0
+
+ print ('This command will checkout all dependent branches and run '
+ '"git cl upload".')
+ ask_for_data('[Press enter to continue or ctrl-C to quit]')
+
+ # Add a default patchset title to all upload calls.
+ args.extend(['-t', 'Updated patchset dependency'])
+ # Record all dependents that failed to upload.
+ failures = {}
+ # Go through all dependents, checkout the branch and upload.
+ try:
+ for dependent_branch in dependents:
+ print
+ print '--------------------------------------'
+ print 'Running "git cl upload" from %s:' % dependent_branch
+ RunGit(['checkout', '-q', dependent_branch])
+ print
+ try:
+ if CMDupload(OptionParser(), args) != 0:
+ print 'Upload failed for %s!' % dependent_branch
+ failures[dependent_branch] = 1
+ except: # pylint: disable=W0702
+ failures[dependent_branch] = 1
+ print
+ finally:
+ # Swap back to the original root branch.
+ RunGit(['checkout', '-q', root_branch])
+
+ print
+ print 'Upload complete for dependent branches!'
+ for dependent_branch in dependents:
+ upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
+ print ' %s : %s' % (dependent_branch, upload_status)
+ print
+
+ return 0
+
+
def CMDstatus(parser, args):
"""Show status of changelists.
@@ -2197,7 +2297,11 @@
parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
help='Send the patchset to do a CQ dry run right after '
'upload.')
+ parser.add_option('--dependencies', action='store_true',
+ help='Uploads CLs of all the local branches that depend on '
+ 'the current branch')
+ orig_args = args
add_git_similarity(parser)
auth.add_auth_options(parser)
(options, args) = parser.parse_args(args)
@@ -2282,6 +2386,16 @@
options.verbose,
sys.stdout)
+ # Upload all dependencies if specified.
+ if options.dependencies:
+ print
+ print '--dependencies has been specified.'
+ print 'All dependent local branches will be re-uploaded.'
+ print
+ # Remove the dependencies flag from args so that we do not end up in a
+ # loop.
+ orig_args.remove('--dependencies')
+ upload_branch_deps(cl, orig_args)
return ret
diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py
index b19e45c..293523d 100755
--- a/tests/git_cl_test.py
+++ b/tests/git_cl_test.py
@@ -696,6 +696,54 @@
squash=True,
expected_upstream_ref='origin/master')
+ def test_upload_branch_deps(self):
+ def mock_run_git(*args, **_kwargs):
+ if args[0] == ['for-each-ref',
+ '--format=%(refname:short) %(upstream:short)',
+ 'refs/heads']:
+ # Create a local branch dependency tree that looks like this:
+ # test1 -> test2 -> test3 -> test4 -> test5
+ # -> test3.1
+ # test6 -> test0
+ branch_deps = [
+ 'test2 test1', # test1 -> test2
+ 'test3 test2', # test2 -> test3
+ 'test3.1 test2', # test2 -> test3.1
+ 'test4 test3', # test3 -> test4
+ 'test5 test4', # test4 -> test5
+ 'test6 test0', # test0 -> test6
+ 'test7', # test7
+ ]
+ return '\n'.join(branch_deps)
+ self.mock(git_cl, 'RunGit', mock_run_git)
+
+ class RecordCalls:
+ times_called = 0
+ record_calls = RecordCalls()
+ def mock_CMDupload(*args, **_kwargs):
+ record_calls.times_called += 1
+ return 0
+ self.mock(git_cl, 'CMDupload', mock_CMDupload)
+
+ self.calls = [
+ (('[Press enter to continue or ctrl-C to quit]',), ''),
+ ]
+
+ class MockChangelist():
+ def __init__(self):
+ pass
+ def GetBranch(self):
+ return 'test1'
+ def GetIssue(self):
+ return '123'
+ def GetPatchset(self):
+ return '1001'
+
+ ret = git_cl.upload_branch_deps(MockChangelist(), [])
+ # CMDupload should have been called 5 times because of 5 dependent branches.
+ self.assertEquals(5, record_calls.times_called)
+ self.assertEquals(0, ret)
+
def test_config_gerrit_download_hook(self):
self.mock(git_cl, 'FindCodereviewSettingsFile', CodereviewSettingsFileMock)
def ParseCodereviewSettingsContent(content):