Skip to content

Commit

Permalink
Fix gclient branch ref mangling and allow --force branch switches.
Browse files Browse the repository at this point in the history
[email protected], [email protected]
BUG=410959

Review URL: https://codereview.chromium.org/549733002

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@291883 0039d316-1c4b-4281-b951-d872f2087c98
  • Loading branch information
[email protected] committed Sep 9, 2014
1 parent 931b6c6 commit 6e7202b
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 25 deletions.
62 changes: 43 additions & 19 deletions gclient_scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,14 @@ def update(self, options, args, file_list):
verbose = ['--verbose']
printed_path = True

if revision.startswith('refs/'):
rev_type = "branch"
elif revision.startswith(self.remote + '/'):
remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
if remote_ref:
# Rewrite remote refs to their local equivalents.
revision = 'refs/remotes/' + revision
revision = ''.join(remote_ref)
rev_type = "branch"
elif revision.startswith('refs/'):
# Local branch? We probably don't want to support, since DEPS should
# always specify branches as they are in the upstream repo.
rev_type = "branch"
else:
# hash is also a tag, only make a distinction at checkout
Expand All @@ -393,10 +396,6 @@ def update(self, options, args, file_list):
except subprocess2.CalledProcessError:
self._DeleteOrMove(options.force)
self._Clone(revision, url, options)
if deps_revision and deps_revision.startswith('branch-heads/'):
deps_branch = deps_revision.replace('branch-heads/', '')
self._Capture(['branch', deps_branch, deps_revision])
self._Checkout(options, deps_branch, quiet=True)
if file_list is not None:
files = self._Capture(['ls-files']).splitlines()
file_list.extend([os.path.join(self.checkout_path, f) for f in files])
Expand Down Expand Up @@ -451,12 +450,16 @@ def update(self, options, args, file_list):
# 2) current branch is tracking a remote branch with local committed
# changes, but the DEPS file switched to point to a hash
# - rebase those changes on top of the hash
# 3) current branch is tracking a remote branch w/or w/out changes,
# no switch
# 3) current branch is tracking a remote branch w/or w/out changes, and
# no DEPS switch
# - see if we can FF, if not, prompt the user for rebase, merge, or stop
# 4) current branch is tracking a remote branch, switches to a different
# remote branch
# - exit
# 4) current branch is tracking a remote branch, but DEPS switches to a
# different remote branch, and
# a) current branch has no local changes, and --force:
# - checkout new branch
# b) current branch has local changes, and --force and --reset:
# - checkout new branch
# c) otherwise exit

# GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
# a tracking branch
Expand Down Expand Up @@ -534,17 +537,37 @@ def update(self, options, args, file_list):
newbase=revision, printed_path=printed_path,
merge=options.merge)
printed_path = True
elif revision.replace('heads', 'remotes/' + self.remote) != upstream_branch:
elif remote_ref and ''.join(remote_ref) != upstream_branch:
# case 4
new_base = revision.replace('heads', 'remotes/' + self.remote)
new_base = ''.join(remote_ref)
if not printed_path:
self.Print('_____ %s%s' % (self.relpath, rev_str), timestamp=False)
switch_error = ("Switching upstream branch from %s to %s\n"
switch_error = ("Could not switch upstream branch from %s to %s\n"
% (upstream_branch, new_base) +
"Please merge or rebase manually:\n" +
"Please use --force or merge or rebase manually:\n" +
"cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
"OR git checkout -b <some new branch> %s" % new_base)
raise gclient_utils.Error(switch_error)
force_switch = False
if options.force:
try:
self._CheckClean(rev_str)
# case 4a
force_switch = True
except gclient_utils.Error as e:
if options.reset:
# case 4b
force_switch = True
else:
switch_error = '%s\n%s' % (e.message, switch_error)
if force_switch:
self.Print("Switching upstream branch from %s to %s" %
(upstream_branch, new_base))
switch_branch = 'gclient_' + remote_ref[1]
self._Capture(['branch', '-f', switch_branch, new_base])
self._Checkout(options, switch_branch, force=True, quiet=True)
else:
# case 4c
raise gclient_utils.Error(switch_error)
else:
# case 3 - the default case
if files is not None:
Expand Down Expand Up @@ -870,7 +893,8 @@ def _Clone(self, revision, url, options):
if template_dir:
gclient_utils.rmtree(template_dir)
self._UpdateBranchHeads(options, fetch=True)
self._Checkout(options, revision.replace('refs/heads/', ''), quiet=True)
remote_ref = scm.GIT.RefToRemoteRef(revision, self.remote)
self._Checkout(options, ''.join(remote_ref or revision), quiet=True)
if self._GetCurrentBranch() is None:
# Squelch git's very verbose detached HEAD warning and use our own
self.Print(
Expand Down
25 changes: 24 additions & 1 deletion scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,12 +352,35 @@ def FetchUpstreamTuple(cwd):
upstream_branch = None
return remote, upstream_branch

@staticmethod
def RefToRemoteRef(ref, remote=None):
"""Convert a checkout ref to the equivalent remote ref.
Returns:
A tuple of the remote ref's (common prefix, unique suffix), or None if it
doesn't appear to refer to a remote ref (e.g. it's a commit hash).
"""
# TODO(mmoss): This is just a brute-force mapping based of the expected git
# config. It's a bit better than the even more brute-force replace('heads',
# ...), but could still be smarter (like maybe actually using values gleaned
# from the git config).
m = re.match('^(refs/(remotes/)?)?branch-heads/', ref or '')
if m:
return ('refs/remotes/branch-heads/', ref.replace(m.group(0), ''))
if remote:
m = re.match('^((refs/)?remotes/)?%s/|(refs/)?heads/' % remote, ref or '')
if m:
return ('refs/remotes/%s/' % remote, ref.replace(m.group(0), ''))
return None

@staticmethod
def GetUpstreamBranch(cwd):
"""Gets the current branch's upstream branch."""
remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
if remote != '.' and upstream_branch:
upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
remote_ref = GIT.RefToRemoteRef(upstream_branch, remote)
if remote_ref:
upstream_branch = ''.join(remote_ref)
return upstream_branch

@staticmethod
Expand Down
18 changes: 13 additions & 5 deletions tests/gclient_scm_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1561,7 +1561,7 @@ def testUpdateCloneOnBranch(self):

rmtree(origin_root_dir)

def testUpdateCloneOnDetachedBranch(self):
def testUpdateCloneOnFetchedRemoteBranch(self):
if not self.enabled:
return
options = self.Options()
Expand Down Expand Up @@ -1593,7 +1593,7 @@ def testUpdateCloneOnDetachedBranch(self):

rmtree(origin_root_dir)

def testUpdateCloneOnBranchHead(self):
def testUpdateCloneOnTrueRemoteBranch(self):
if not self.enabled:
return
options = self.Options()
Expand All @@ -1618,9 +1618,17 @@ def testUpdateCloneOnBranchHead(self):
self.assertEquals(file_list, expected_file_list)
self.assertEquals(scm.revinfo(options, (), None),
'9a51244740b25fa2ded5252ca00a3178d3f665a9')
self.assertEquals(self.getCurrentBranch(), 'feature')
self.checkNotInStdout(
'Checked out refs/heads/feature to a detached HEAD')
# @refs/heads/feature is AKA @refs/remotes/origin/feature in the clone, so
# should be treated as such by gclient.
# TODO(mmoss): Though really, we should only allow DEPS to specify branches
# as they are known in the upstream repo, since the mapping into the local
# repo can be modified by users (or we might even want to change the gclient
# defaults at some point). But that will take more work to stop using
# refs/remotes/ everywhere that we do (and to stop assuming a DEPS ref will
# always resolve locally, like when passing them to show-ref or rev-list).
self.assertEquals(self.getCurrentBranch(), None)
self.checkInStdout(
'Checked out refs/remotes/origin/feature to a detached HEAD')

rmtree(origin_root_dir)

Expand Down
45 changes: 45 additions & 0 deletions tests/scm_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def testMembersChanged(self):
'IsWorkTreeDirty',
'MatchSvnGlob',
'ParseGitSvnSha1',
'RefToRemoteRef',
'ShortBranchName',
]
# If this test fails, you should add the relevant test.
Expand All @@ -122,6 +123,50 @@ def testMatchSvnGlob(self):
'branches/*:refs/remotes/*',
True), 'refs/remotes/bleeding_edge')

def testRefToRemoteRefNoRemote(self):
refs = {
# local ref for upstream branch-head
'refs/remotes/branch-heads/1234': ('refs/remotes/branch-heads/',
'1234'),
# upstream ref for branch-head
'refs/branch-heads/1234': ('refs/remotes/branch-heads/', '1234'),
# could be either local or upstream ref, assumed to refer to
# upstream, but probably don't want to encourage refs like this.
'branch-heads/1234': ('refs/remotes/branch-heads/', '1234'),
# actively discouraging refs like this, should prepend with 'refs/'
'remotes/branch-heads/1234': None,
# might be non-"branch-heads" upstream branches, but can't resolve
# without knowing the remote.
'refs/heads/1234': None,
'heads/1234': None,
# underspecified, probably intended to refer to a local branch
'1234': None,
}
for k, v in refs.items():
r = scm.GIT.RefToRemoteRef(k)
self.assertEqual(r, v, msg='%s -> %s, expected %s' % (k, r, v))

def testRefToRemoteRefWithRemote(self):
remote = 'origin'
refs = {
# This shouldn't be any different from the NoRemote() version.
'refs/branch-heads/1234': ('refs/remotes/branch-heads/', '1234'),
# local refs for upstream branch
'refs/remotes/%s/foobar' % remote: ('refs/remotes/%s/' % remote,
'foobar'),
'%s/foobar' % remote: ('refs/remotes/%s/' % remote, 'foobar'),
# upstream ref for branch
'refs/heads/foobar': ('refs/remotes/%s/' % remote, 'foobar'),
# could be either local or upstream ref, assumed to refer to
# upstream, but probably don't want to encourage refs like this.
'heads/foobar': ('refs/remotes/%s/' % remote, 'foobar'),
# underspecified, probably intended to refer to a local branch
'foobar': None,
}
for k, v in refs.items():
r = scm.GIT.RefToRemoteRef(k, remote)
self.assertEqual(r, v, msg='%s -> %s, expected %s' % (k, r, v))


class RealGitTest(fake_repos.FakeReposTestBase):
def setUp(self):
Expand Down

0 comments on commit 6e7202b

Please sign in to comment.