[git cl]Add precheck function for stacked changes upload.

High-level rough draft: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4166282

Bug: b/265929888

Change-Id: I7881ade0ea97d7537e1dd40ab484ee5ef828aa34
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4175861
Commit-Queue: Joanna Wang <jojwang@chromium.org>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
changes/61/4175861/16
Joanna Wang 2 years ago committed by LUCI CQ
parent 39811b1915
commit 18de1f68e6

@ -155,6 +155,11 @@ assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
# Maximum number of branches in a stack that can be traversed and uploaded
# at once. Picked arbitrarily.
_MAX_STACKED_BRANCHES_UPLOAD = 20
class GitPushError(Exception):
pass
@ -2251,7 +2256,10 @@ class Changelist(object):
return 0
def _GerritCommitMsgHookCheck(self, offer_removal):
@staticmethod
def _GerritCommitMsgHookCheck(offer_removal):
# type: (bool) -> None
"""Checks for the gerrit's commit-msg hook and removes it if necessary."""
hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
if not os.path.exists(hook):
return
@ -4514,6 +4522,9 @@ def CMDupload(parser, args):
parser.add_option('--no-python2-post-upload-hooks',
action='store_true',
help='Only run post-upload hooks in Python 3.')
parser.add_option('--stacked-exp',
action='store_true',
help=optparse.SUPPRESS_HELP)
orig_args = args
(options, args) = parser.parse_args(args)
@ -4554,6 +4565,12 @@ def CMDupload(parser, args):
# Load default for user, repo, squash=true, in this order.
options.squash = settings.GetSquashGerritUploads()
if options.stacked_exp:
orig_args.remove('--stacked-exp')
UploadAllSquashed(options, orig_args)
return 0
cl = Changelist(branchref=options.target_branch)
# Warm change details cache now to avoid RPCs later, reducing latency for
# developers.
@ -4589,6 +4606,106 @@ def CMDupload(parser, args):
return ret
def UploadAllSquashed(options, orig_args):
# type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
"""Uploads the current and upstream branches (if necessary)."""
_cls, _cherry_pick_current = _UploadAllPrecheck(options, orig_args)
# TODO(b/265929888): parse cls and create commits.
def _UploadAllPrecheck(options, orig_args):
# type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
"""Checks the state of the tree and gives the user uploading options
Returns: A tuple of the ordered list of changes that have new commits
since their last upload and a boolean of whether the user wants to
cherry-pick and upload the current branch instead of uploading all cls.
"""
branch_ref = None
cls = []
must_upload_upstream = False
Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
while True:
if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
DieWithError(
'More than %s branches in the stack have not been uploaded.\n'
'Are your branches in a misconfigured state?\n'
'If not, please upload some upstream changes first.' %
(_MAX_STACKED_BRANCHES_UPLOAD))
cl = Changelist(branchref=branch_ref)
cls.append(cl)
origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(cl.GetBranch())
upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
branch_ref = upstream_branch_ref # set branch for next run.
# Case 1: We've reached the beginning of the tree.
if origin != '.':
break
upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
upstream_branch,
LAST_UPLOAD_HASH_CONFIG_KEY)
# Case 2: If any upstream branches have never been uploaded,
# the user MUST upload them.
if not upstream_last_upload:
must_upload_upstream = True
continue
base_commit = cl.GetCommonAncestorWithUpstream()
# Case 3: If upstream's last_upload == cl.base_commit we do
# not need to upload any more upstreams from this point on.
# (Even if there may be diverged branches higher up the tree)
if base_commit == upstream_last_upload:
break
# Case 4: If upstream's last_upload < cl.base_commit we are
# uploading cl and upstream_cl.
# Continue up the tree to check other branch relations.
if scm.GIT.IsAncestor(None, upstream_last_upload, base_commit):
continue
# Case 5: If cl.base_commit < upstream's last_upload the user
# must rebase before uploading.
if scm.GIT.IsAncestor(None, base_commit, upstream_last_upload):
DieWithError(
'At least one branch in the stack has diverged from its upstream '
'branch and does not contain its upstream\'s last upload.\n'
'Please rebase the stack with `git rebase-update` before uploading.')
# The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer has
# any relation to commits in the tree. Continue up the tree until we hit
# the root.
# We assume all cls in the stack have the same auth requirements and only
# check this once.
cls[0].EnsureAuthenticated(force=options.force)
cherry_pick = False
if len(cls) > 1:
message = ''
if len(orig_args):
message = ('options %s will be used for all uploads.\n' % orig_args)
if must_upload_upstream:
confirm_or_exit('\n' + message +
'There are upstream branches that must be uploaded.\n')
else:
answer = gclient_utils.AskForData(
'\n' + message +
'Press enter to update branches %s.\nOr type `n` to upload only '
'`%s` cherry-picked on %s\'s last upload:' %
([cl.branch for cl in cls], cls[0].branch, cls[1].branch))
if answer.lower() == 'n':
cherry_pick = True
return cls, cherry_pick
@subcommand.usage('--description=<description file>')
@metrics.collector.collect_metrics('git cl split')
def CMDsplit(parser, args):

@ -1514,6 +1514,163 @@ class TestGitCl(unittest.TestCase):
external_parent='newparent',
)
@mock.patch(
'git_cl.Changelist._GerritCommitMsgHookCheck', lambda offer_removal: None)
@mock.patch('git_cl.Changelist.FetchUpstreamTuple')
@mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
@mock.patch('git_cl.Changelist.GetBranch')
@mock.patch('git_cl.Changelist.GetRemoteBranch')
@mock.patch('scm.GIT.IsAncestor')
@mock.patch('gclient_utils.AskForData')
def test_upload_all_precheck_long_chain(self, mockAskForData, mockIsAncestor,
mockGetRemoteBranch, mockGetBranch,
mockGetCommonAncestorWithUpstream,
mockFetchUpstreamTuple, *_mocks):
mockGetRemoteBranch.return_value = ('origin', 'refs/remotes/origin/main')
mockGetBranch.side_effect = [
'current', 'upstream3', 'upstream2', 'upstream1', 'main'
]
mockGetCommonAncestorWithUpstream.side_effect = [
'commit3.5', 'commit2.5', 'commit1.5', 'commit0.5'
]
mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
('.', 'refs/heads/upstream2'),
('.', 'refs/heads/upstream1'),
('origin', 'refs/heads/main')]
options = optparse.Values()
options.force = False
orig_args = ['--preserve-tryjobs', '--chicken']
# Case 2: upstream3 has never been uploaded.
# (so no LAST_UPLOAD_HASH_CONIFG_KEY)
# Case 4: upstream2's last_upload is behind upstream3's base_commit
self.mockGit.config['branch.upstream1.%s' %
git_cl.LAST_UPLOAD_HASH_CONFIG_KEY] = 'commit2.3'
mockIsAncestor.side_effect = [True]
# Case 3: upstream1's last_upload matches upstream2's base_commit
self.mockGit.config['branch.upstream1.%s' %
git_cl.LAST_UPLOAD_HASH_CONFIG_KEY] = 'commit1.5'
cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
self.assertFalse(cherry_pick)
mockAskForData.assert_called_once_with(
"\noptions ['--preserve-tryjobs', '--chicken'] will be used for all "
"uploads.\nThere are upstream branches that must be uploaded.\n"
"Press Enter to confirm, or Ctrl+C to abort")
self.assertEqual(len(cls), 4)
@mock.patch(
'git_cl.Changelist._GerritCommitMsgHookCheck', lambda offer_removal: None)
@mock.patch('git_cl.Changelist.FetchUpstreamTuple')
@mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
@mock.patch('git_cl.Changelist.GetBranch')
@mock.patch('git_cl.Changelist.GetRemoteBranch')
@mock.patch('scm.GIT.IsAncestor')
@mock.patch('gclient_utils.AskForData')
def test_upload_all_precheck_must_rebase(self, mockAskForData, mockIsAncestor,
mockGetRemoteBranch, mockGetBranch,
mockGetCommonAncestorWithUpstream,
mockFetchUpstreamTuple, *_mocks):
options = optparse.Values()
options.force = False
orig_args = ['--preserve-tryjobs', '--chicken']
mockGetRemoteBranch.return_value = ('origin', 'refs/remotes/origin/main')
mockGetBranch.side_effect = [
'current', 'upstream3', 'upstream2', 'upstream1', 'main'
]
mockGetCommonAncestorWithUpstream.side_effect = [
'commit3.5', 'commit2.5', 'commit1.5', 'commit0.5'
]
mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
('.', 'refs/heads/upstream2'),
('.', 'refs/heads/upstream1'),
('origin', 'refs/heads/main')]
# Case 5: current's base_commit is behind upstream3's last_upload.
self.mockGit.config['branch.upstream3.%s' %
git_cl.LAST_UPLOAD_HASH_CONFIG_KEY] = 'commit3.7'
mockIsAncestor.side_effect = [False, True]
with self.assertRaises(SystemExitMock):
git_cl._UploadAllPrecheck(options, orig_args)
@mock.patch(
'git_cl.Changelist._GerritCommitMsgHookCheck', lambda offer_removal: None)
@mock.patch('git_cl.Changelist.FetchUpstreamTuple')
@mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
@mock.patch('git_cl.Changelist.GetBranch')
@mock.patch('git_cl.Changelist.GetRemoteBranch')
@mock.patch('scm.GIT.IsAncestor')
@mock.patch('gclient_utils.AskForData')
def test_upload_all_precheck_hit_main(self, mockAskForData, mockIsAncestor,
mockGetRemoteBranch, mockGetBranch,
mockGetCommonAncestorWithUpstream,
mockFetchUpstreamTuple, *_mocks):
options = optparse.Values()
options.force = False
orig_args = ['--preserve-tryjobs', '--chicken']
mockGetRemoteBranch.return_value = ('origin', 'refs/remotes/origin/main')
mockGetBranch.return_value = ['current', 'upstream3', 'main']
mockGetCommonAncestorWithUpstream.side_effect = ['commit3.5', 'commit0.5']
mockFetchUpstreamTuple.side_effect = [('.', 'refs/heads/upstream3'),
('origin', 'refs/heads/main')]
mockIsAncestor.return_value = True
# Test user wants to cherry pick
mockAskForData.return_value = 'n'
# Give upstream3 a last upload hash
self.mockGit.config['branch.upstream3.%s' %
git_cl.LAST_UPLOAD_HASH_CONFIG_KEY] = 'commit3.4'
# Case 1: We hit the main branch
cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
self.assertTrue(cherry_pick)
self.assertEqual(len(cls), 2)
mockAskForData.assert_called_once_with(
"\noptions ['--preserve-tryjobs', '--chicken'] will be used for all "
"uploads.\n"
"Press enter to update branches [None, 'upstream3'].\n"
"Or type `n` to upload only `None` cherry-picked on upstream3's last "
"upload:")
@mock.patch(
'git_cl.Changelist._GerritCommitMsgHookCheck', lambda offer_removal: None)
@mock.patch('git_cl.Changelist.FetchUpstreamTuple')
@mock.patch('git_cl.Changelist.GetCommonAncestorWithUpstream')
@mock.patch('git_cl.Changelist.GetBranch')
@mock.patch('git_cl.Changelist.GetRemoteBranch')
@mock.patch('scm.GIT.IsAncestor')
@mock.patch('gclient_utils.AskForData')
def test_upload_all_precheck_one_change(self, mockAskForData, mockIsAncestor,
mockGetRemoteBranch, mockGetBranch,
mockGetCommonAncestorWithUpstream,
mockFetchUpstreamTuple, *_mocks):
options = optparse.Values()
options.force = False
orig_args = ['--preserve-tryjobs', '--chicken']
mockGetRemoteBranch.return_value = ('origin', 'refs/remotes/origin/main')
mockGetBranch.return_value = ['current', 'main']
mockGetCommonAncestorWithUpstream.side_effect = ['commit3.5']
mockFetchUpstreamTuple.side_effect = [('', 'refs/heads/main')]
mockIsAncestor.return_value = True
# Case 1: We hit the main branch
cls, cherry_pick = git_cl._UploadAllPrecheck(options, orig_args)
self.assertFalse(cherry_pick)
self.assertEqual(len(cls), 1)
mockAskForData.assert_not_called()
@mock.patch('git_cl.RunGit')
@mock.patch('git_cl.CMDupload')
@mock.patch('sys.stdin', StringIO('\n'))

Loading…
Cancel
Save