gclient: Fix applying patches to branch heads.

Bug: 956807
Change-Id: I2eed6db1f338806812fca778986d51b0a007eddd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1592577
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
Reviewed-by: Andrii Shyshkalov <tandrii@chromium.org>
changes/77/1592577/13
Edward Lemur 6 years ago committed by Commit Bot
parent 1c739917c1
commit 8c66565649

@ -372,25 +372,25 @@ class GitWrapper(SCMWrapper):
'Candidate refs were %s.' % (commit, remote_refs)) 'Candidate refs were %s.' % (commit, remote_refs))
return None return None
def apply_patch_ref(self, patch_repo, patch_ref, target_branch, options, def apply_patch_ref(self, patch_repo, patch_ref, target_ref, options,
file_list): file_list):
"""Apply a patch on top of the revision we're synced at. """Apply a patch on top of the revision we're synced at.
The patch ref is given by |patch_repo|@|patch_ref|, and the current revision The patch ref is given by |patch_repo|@|patch_ref|, and the current revision
is |base_rev|. is |base_rev|.
We also need the |target_branch| that the patch was uploaded against. We use We also need the |target_ref| that the patch was uploaded against. We use
it to find a merge base between |patch_rev| and |base_rev|, so we can find it to find a merge base between |patch_rev| and |base_rev|, so we can find
what commits constitute the patch: what commits constitute the patch:
Graphically, it looks like this: Graphically, it looks like this:
... -> merge_base -> [possibly already landed commits] -> target_branch ... -> merge_base -> [possibly already landed commits] -> target_ref
\ \
-> [possibly not yet landed dependent CLs] -> patch_rev -> [possibly not yet landed dependent CLs] -> patch_rev
Next, we apply the commits |merge_base..patch_rev| on top of whatever is Next, we apply the commits |merge_base..patch_rev| on top of whatever is
currently checked out, denoted |base_rev|. Typically, it'd be a revision currently checked out, denoted |base_rev|. Typically, it'd be a revision
from |target_branch|, but this is not required. from |target_ref|, but this is not required.
Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|: Graphically, we cherry pick |merge_base..patch_rev| on top of |base_rev|:
@ -402,7 +402,7 @@ class GitWrapper(SCMWrapper):
Args: Args:
patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar' patch_repo: The patch origin. e.g. 'https://foo.googlesource.com/bar'
patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'. patch_ref: The ref to the patch. e.g. 'refs/changes/1234/34/1'.
target_branch: The branch the patch was uploaded against. target_ref: The ref the patch was uploaded against.
e.g. 'refs/heads/master' or 'refs/heads/infra/config'. e.g. 'refs/heads/master' or 'refs/heads/infra/config'.
options: The options passed to gclient. options: The options passed to gclient.
file_list: A list where modified files will be appended. file_list: A list where modified files will be appended.
@ -416,25 +416,49 @@ class GitWrapper(SCMWrapper):
base_rev = self._Capture(['rev-parse', 'HEAD']) base_rev = self._Capture(['rev-parse', 'HEAD'])
if target_branch: if not target_ref:
# Convert the target branch to a remote ref if possible. target_ref = self._GetTargetBranchForCommit(base_rev) or base_rev
remote_ref = scm.GIT.RefToRemoteRef(target_branch, self.remote) elif not target_ref.startswith(('refs/heads/', 'refs/branch-heads/')):
if remote_ref: # TODO(ehmaldonado): Support all refs.
target_branch = ''.join(remote_ref) raise gclient_utils.Error(
'target_ref must be in refs/heads/** or refs/branch-heads/**. '
'Got %s instead.' % target_ref)
elif target_ref == 'refs/heads/master':
# We handle refs/heads/master separately because bot_update treats it
# differently than other refs: it will fetch refs/heads/foo to
# refs/heads/foo, but refs/heads/master to refs/remotes/origin/master.
# It's not strictly necessary, but it simplifies the rest of the code.
target_ref = 'refs/remotes/%s/master' % self.remote
elif scm.GIT.IsValidRevision(self.checkout_path, target_ref):
# The target ref for the change is already available, so we don't need to
# do anything.
# This is a common case. When applying a patch to a top-level solution,
# bot_update will fetch the target ref for the change and sync the
# solution to it.
pass
else: else:
target_branch = self._GetTargetBranchForCommit(base_rev) # The target ref is not available. We check next for a remote version of
# it (e.g. refs/remotes/origin/<branch>) and fetch it if it's not
# Fallback to the commit we got. # available.
# This means that apply_path_ref will try to find the merge-base between the self.Print('%s is not an existing ref.' % target_ref)
# patch and the commit (which is most likely the commit) and cherry-pick original_target_ref = target_ref
# everything in between. target_ref = ''.join(scm.GIT.RefToRemoteRef(target_ref, self.remote))
if not target_branch: self.Print('Trying with %s' % target_ref)
target_branch = base_rev if not scm.GIT.IsValidRevision(self.checkout_path, target_ref):
self.Print(
'%s is not an existing ref either. Will proceed to fetch it.'
% target_ref)
url, _ = gclient_utils.SplitUrlRevision(self.url)
mirror = self._GetMirror(url, options, target_ref)
if mirror:
self._UpdateMirrorIfNotContains(
mirror, options, 'branch', target_ref, original_target_ref)
self._Fetch(options, refspec=target_ref)
self.Print('===Applying patch ref===') self.Print('===Applying patch ref===')
self.Print('Patch ref is %r @ %r. Target branch for patch is %r. ' self.Print('Patch ref is %r @ %r. Target branch for patch is %r. '
'Current HEAD is %r. Current dir is %r' % ( 'Current HEAD is %r. Current dir is %r' % (
patch_repo, patch_ref, target_branch, base_rev, patch_repo, patch_ref, target_ref, base_rev,
self.checkout_path)) self.checkout_path))
self._Capture(['reset', '--hard']) self._Capture(['reset', '--hard'])
self._Capture(['fetch', patch_repo, patch_ref]) self._Capture(['fetch', patch_repo, patch_ref])
@ -446,9 +470,9 @@ class GitWrapper(SCMWrapper):
else: else:
# Find the merge-base between the branch_rev and patch_rev to find out # Find the merge-base between the branch_rev and patch_rev to find out
# the changes we need to cherry-pick on top of base_rev. # the changes we need to cherry-pick on top of base_rev.
merge_base = self._Capture(['merge-base', target_branch, patch_rev]) merge_base = self._Capture(['merge-base', target_ref, patch_rev])
self.Print('Merge base of %s and %s is %s' % ( self.Print('Merge base of %s and %s is %s' % (
target_branch, patch_rev, merge_base)) target_ref, patch_rev, merge_base))
if merge_base == patch_rev: if merge_base == patch_rev:
# If the merge-base is patch_rev, it means patch_rev is already part # If the merge-base is patch_rev, it means patch_rev is already part
# of the history, so just check it out. # of the history, so just check it out.
@ -467,9 +491,9 @@ class GitWrapper(SCMWrapper):
except subprocess2.CalledProcessError as e: except subprocess2.CalledProcessError as e:
self.Print('Failed to apply patch.') self.Print('Failed to apply patch.')
self.Print('Patch ref is %r @ %r. Target branch for patch is %r. ' self.Print('Patch ref is %r @ %r. Target ref for patch is %r. '
'Current HEAD is %r. Current dir is %r' % ( 'Current HEAD is %r. Current dir is %r' % (
patch_repo, patch_ref, target_branch, base_rev, patch_repo, patch_ref, target_ref, base_rev,
self.checkout_path)) self.checkout_path))
self.Print('git returned non-zero exit status %s:\n%s' % ( self.Print('git returned non-zero exit status %s:\n%s' % (
e.returncode, e.stderr)) e.returncode, e.stderr))

@ -369,9 +369,14 @@ class GIT(object):
sha_only: Fail unless rev is a sha hash. sha_only: Fail unless rev is a sha hash.
""" """
if sys.platform.startswith('win'):
# Windows .bat scripts use ^ as escape sequence, which means we have to
# escape it with itself for every .bat invocation.
needle = '%s^^^^{commit}' % rev
else:
needle = '%s^{commit}' % rev
try: try:
sha = GIT.Capture(['rev-parse', '--verify', '%s^{commit}' % rev], sha = GIT.Capture(['rev-parse', '--verify', needle], cwd=cwd)
cwd=cwd)
if sha_only: if sha_only:
return sha == rev.lower() return sha == rev.lower()
return True return True

@ -1201,8 +1201,8 @@ class GerritChangesTest(fake_repos.FakeReposTestBase):
scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.') scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
file_list = [] file_list = []
# Sync to origin/feature # Sync to remote's refs/heads/feature
self.options.revision = 'origin/feature' self.options.revision = 'refs/heads/feature'
scm.update(self.options, None, file_list) scm.update(self.options, None, file_list)
self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir)) self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))
@ -1219,7 +1219,7 @@ class GerritChangesTest(fake_repos.FakeReposTestBase):
scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.') scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
file_list = [] file_list = []
# Sync to origin/feature on an old revision # Sync to remote's refs/heads/feature on an old revision
self.options.revision = self.githash('repo_1', 7) self.options.revision = self.githash('repo_1', 7)
scm.update(self.options, None, file_list) scm.update(self.options, None, file_list)
self.assertEqual(self.githash('repo_1', 7), self.gitrevparse(self.root_dir)) self.assertEqual(self.githash('repo_1', 7), self.gitrevparse(self.root_dir))
@ -1229,8 +1229,8 @@ class GerritChangesTest(fake_repos.FakeReposTestBase):
file_list) file_list)
# We shouldn't have rebased on top of 2 (which is the merge base between # We shouldn't have rebased on top of 2 (which is the merge base between
# origin/master and the change) but on top of 7 (which is the merge base # remote's master branch and the change) but on top of 7 (which is the
# between origin/feature and the change). # merge base between remote's feature branch and the change).
self.assertCommits([1, 2, 7, 10]) self.assertCommits([1, 2, 7, 10])
self.assertEqual(self.githash('repo_1', 7), self.gitrevparse(self.root_dir)) self.assertEqual(self.githash('repo_1', 7), self.gitrevparse(self.root_dir))
@ -1238,18 +1238,18 @@ class GerritChangesTest(fake_repos.FakeReposTestBase):
scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.') scm = gclient_scm.GitWrapper(self.url, self.root_dir, '.')
file_list = [] file_list = []
# Sync to the hash instead of 'origin/feature' # Sync to the hash instead of remote's refs/heads/feature.
self.options.revision = self.githash('repo_1', 9) self.options.revision = self.githash('repo_1', 9)
scm.update(self.options, None, file_list) scm.update(self.options, None, file_list)
self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir)) self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))
# Apply refs/changes/34/1234/1, created for branch 'origin/master' on top of # Apply refs/changes/34/1234/1, created for remote's master branch on top of
# 'origin/feature'. # remote's feature branch.
scm.apply_patch_ref(self.url, 'refs/changes/35/1235/1', 'origin/master', scm.apply_patch_ref(self.url, 'refs/changes/35/1235/1', 'refs/heads/master',
self.options, file_list) self.options, file_list)
# Commits 5 and 6 are part of the patch, and commits 1, 2, 7, 8 and 9 are # Commits 5 and 6 are part of the patch, and commits 1, 2, 7, 8 and 9 are
# part of 'origin/feature'. # part of remote's feature branch.
self.assertCommits([1, 2, 5, 6, 7, 8, 9]) self.assertCommits([1, 2, 5, 6, 7, 8, 9])
self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir)) self.assertEqual(self.githash('repo_1', 9), self.gitrevparse(self.root_dir))

Loading…
Cancel
Save