Commit 091f5ac0 authored by Josip Sokcevic's avatar Josip Sokcevic Committed by LUCI CQ

Use real default branch in gclient

Currently, gclient sync assumes the default branch is master, and
it doesn't work at all if such branch doesn't exist. This change queries
local git copy to get remote HEAD. If local git version is not
available, it queries remote git server using ls-remote.

This change requires git version 2.28 (depot_tools comes with 2.29).

R=ehmaldonado@chromium.org

Bug: 1156318
Change-Id: Id348e0f1004093f395139e8f4d62adb66b94ca9c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/2628359
Commit-Queue: Josip Sokcevic <sokcevic@google.com>
Reviewed-by: 's avatarEdward Lesmes <ehmaldonado@chromium.org>
parent 2241db8a
...@@ -473,8 +473,6 @@ class GitWrapper(SCMWrapper): ...@@ -473,8 +473,6 @@ class GitWrapper(SCMWrapper):
self._CheckMinVersion("1.6.6") self._CheckMinVersion("1.6.6")
# If a dependency is not pinned, track the default remote branch.
default_rev = 'refs/remotes/%s/master' % self.remote
url, deps_revision = gclient_utils.SplitUrlRevision(self.url) url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
revision = deps_revision revision = deps_revision
managed = True managed = True
...@@ -487,7 +485,9 @@ class GitWrapper(SCMWrapper): ...@@ -487,7 +485,9 @@ class GitWrapper(SCMWrapper):
revision = deps_revision revision = deps_revision
managed = False managed = False
if not revision: if not revision:
revision = default_rev # If a dependency is not pinned, track the default remote branch.
revision = scm.GIT.GetRemoteHeadRef(self.checkout_path, self.url,
self.remote)
if managed: if managed:
self._DisableHooks() self._DisableHooks()
......
...@@ -111,7 +111,7 @@ class GIT(object): ...@@ -111,7 +111,7 @@ class GIT(object):
return env return env
@staticmethod @staticmethod
def Capture(args, cwd, strip_out=True, **kwargs): def Capture(args, cwd=None, strip_out=True, **kwargs):
env = GIT.ApplyEnvVars(kwargs) env = GIT.ApplyEnvVars(kwargs)
output = subprocess2.check_output( output = subprocess2.check_output(
['git'] + args, cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs) ['git'] + args, cwd=cwd, stderr=subprocess2.PIPE, env=env, **kwargs)
...@@ -193,6 +193,31 @@ class GIT(object): ...@@ -193,6 +193,31 @@ class GIT(object):
except subprocess2.CalledProcessError: except subprocess2.CalledProcessError:
return None return None
@staticmethod
def GetRemoteHeadRef(cwd, url, remote):
"""Returns the full default remote branch reference, e.g.
'refs/remotes/origin/main'."""
if os.path.exists(cwd):
try:
# Try using local git copy first
ref = 'refs/remotes/%s/HEAD' % remote
return GIT.Capture(['symbolic-ref', ref], cwd=cwd)
except subprocess2.CalledProcessError:
pass
try:
# Fetch information from git server
resp = GIT.Capture(['ls-remote', '--symref', url, 'HEAD'])
regex = r'^ref: (.*)\tHEAD$'
for line in resp.split('\n'):
m = re.match(regex, line)
if m:
return ''.join(GIT.RefToRemoteRef(m.group(1), remote))
except subprocess2.CalledProcessError:
pass
# Return default branch
return 'refs/remotes/%s/master' % remote
@staticmethod @staticmethod
def GetBranch(cwd): def GetBranch(cwd):
"""Returns the short branch name, e.g. 'main'.""" """Returns the short branch name, e.g. 'main'."""
......
...@@ -196,6 +196,11 @@ from :3 ...@@ -196,6 +196,11 @@ from :3
stderr=STDOUT, cwd=path).communicate() stderr=STDOUT, cwd=path).communicate()
Popen([GIT, 'config', 'user.name', 'Some User'], stdout=PIPE, Popen([GIT, 'config', 'user.name', 'Some User'], stdout=PIPE,
stderr=STDOUT, cwd=path).communicate() stderr=STDOUT, cwd=path).communicate()
# Set HEAD back to master
Popen([GIT, 'checkout', 'master', '-q'],
stdout=PIPE,
stderr=STDOUT,
cwd=path).communicate()
return True return True
def _GetAskForDataCallback(self, expected_prompt, return_value): def _GetAskForDataCallback(self, expected_prompt, return_value):
...@@ -640,7 +645,7 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase): ...@@ -640,7 +645,7 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase):
self, mockCheckOutput, mockExists, mockIsdir, mockClone): self, mockCheckOutput, mockExists, mockIsdir, mockClone):
mockIsdir.side_effect = lambda path: path == self.base_path mockIsdir.side_effect = lambda path: path == self.base_path
mockExists.side_effect = lambda path: path == self.base_path mockExists.side_effect = lambda path: path == self.base_path
mockCheckOutput.return_value = b'' mockCheckOutput.side_effect = [b'refs/remotes/origin/main', b'', b'']
options = self.Options() options = self.Options()
scm = gclient_scm.GitWrapper( scm = gclient_scm.GitWrapper(
...@@ -648,18 +653,21 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase): ...@@ -648,18 +653,21 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase):
scm.update(options, None, []) scm.update(options, None, [])
env = gclient_scm.scm.GIT.ApplyEnvVars({}) env = gclient_scm.scm.GIT.ApplyEnvVars({})
self.assertEqual( self.assertEqual(mockCheckOutput.mock_calls, [
mockCheckOutput.mock_calls, mock.call(['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'],
[ cwd=self.base_path,
mock.call( env=env,
['git', '-c', 'core.quotePath=false', 'ls-files'], stderr=-1),
cwd=self.base_path, env=env, stderr=-1), mock.call(['git', '-c', 'core.quotePath=false', 'ls-files'],
mock.call( cwd=self.base_path,
['git', 'rev-parse', '--verify', 'HEAD'], env=env,
cwd=self.base_path, env=env, stderr=-1), stderr=-1),
mock.call(['git', 'rev-parse', '--verify', 'HEAD'],
cwd=self.base_path,
env=env,
stderr=-1),
]) ])
mockClone.assert_called_with( mockClone.assert_called_with('refs/remotes/origin/main', self.url, options)
'refs/remotes/origin/master', self.url, options)
self.checkstdout('\n') self.checkstdout('\n')
@mock.patch('gclient_scm.GitWrapper._Clone') @mock.patch('gclient_scm.GitWrapper._Clone')
...@@ -670,7 +678,7 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase): ...@@ -670,7 +678,7 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase):
self, mockCheckOutput, mockExists, mockIsdir, mockClone): self, mockCheckOutput, mockExists, mockIsdir, mockClone):
mockIsdir.side_effect = lambda path: path == self.base_path mockIsdir.side_effect = lambda path: path == self.base_path
mockExists.side_effect = lambda path: path == self.base_path mockExists.side_effect = lambda path: path == self.base_path
mockCheckOutput.return_value = b'' mockCheckOutput.side_effect = [b'refs/remotes/origin/main', b'', b'']
mockClone.side_effect = [ mockClone.side_effect = [
gclient_scm.subprocess2.CalledProcessError( gclient_scm.subprocess2.CalledProcessError(
None, None, None, None, None), None, None, None, None, None),
...@@ -683,18 +691,21 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase): ...@@ -683,18 +691,21 @@ class ManagedGitWrapperTestCaseMock(unittest.TestCase):
scm.update(options, None, []) scm.update(options, None, [])
env = gclient_scm.scm.GIT.ApplyEnvVars({}) env = gclient_scm.scm.GIT.ApplyEnvVars({})
self.assertEqual( self.assertEqual(mockCheckOutput.mock_calls, [
mockCheckOutput.mock_calls, mock.call(['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'],
[ cwd=self.base_path,
mock.call( env=env,
['git', '-c', 'core.quotePath=false', 'ls-files'], stderr=-1),
cwd=self.base_path, env=env, stderr=-1), mock.call(['git', '-c', 'core.quotePath=false', 'ls-files'],
mock.call( cwd=self.base_path,
['git', 'rev-parse', '--verify', 'HEAD'], env=env,
cwd=self.base_path, env=env, stderr=-1), stderr=-1),
mock.call(['git', 'rev-parse', '--verify', 'HEAD'],
cwd=self.base_path,
env=env,
stderr=-1),
]) ])
mockClone.assert_called_with( mockClone.assert_called_with('refs/remotes/origin/main', self.url, options)
'refs/remotes/origin/master', self.url, options)
self.checkstdout('\n') self.checkstdout('\n')
......
...@@ -99,6 +99,26 @@ class GitWrapperTestCase(unittest.TestCase): ...@@ -99,6 +99,26 @@ class GitWrapperTestCase(unittest.TestCase):
r = scm.GIT.RemoteRefToRef(k, remote) r = scm.GIT.RemoteRefToRef(k, remote)
self.assertEqual(r, v, msg='%s -> %s, expected %s' % (k, r, v)) self.assertEqual(r, v, msg='%s -> %s, expected %s' % (k, r, v))
@mock.patch('scm.GIT.Capture')
@mock.patch('os.path.exists', lambda _:True)
def testGetRemoteHeadRefLocal(self, mockCapture):
mockCapture.side_effect = ['refs/remotes/origin/main']
self.assertEqual('refs/remotes/origin/main',
scm.GIT.GetRemoteHeadRef('foo', 'proto://url', 'origin'))
self.assertEqual(mockCapture.call_count, 1)
@mock.patch('scm.GIT.Capture')
@mock.patch('os.path.exists', lambda _:True)
def testGetRemoteHeadRefRemote(self, mockCapture):
mockCapture.side_effect = [
subprocess2.CalledProcessError(1, '', '', '', ''),
'ref: refs/heads/main\tHEAD\n' +
'0000000000000000000000000000000000000000\tHEAD',
]
self.assertEqual('refs/remotes/origin/main',
scm.GIT.GetRemoteHeadRef('foo', 'proto://url', 'origin'))
self.assertEqual(mockCapture.call_count, 2)
class RealGitTest(fake_repos.FakeReposTestBase): class RealGitTest(fake_repos.FakeReposTestBase):
def setUp(self): def setUp(self):
...@@ -180,10 +200,10 @@ class RealGitTest(fake_repos.FakeReposTestBase): ...@@ -180,10 +200,10 @@ class RealGitTest(fake_repos.FakeReposTestBase):
self.assertEqual( self.assertEqual(
(None, None), scm.GIT.FetchUpstreamTuple(self.cwd)) (None, None), scm.GIT.FetchUpstreamTuple(self.cwd))
@mock.patch('scm.GIT.GetRemoteBranches', return_value=['origin/master']) @mock.patch('scm.GIT.GetRemoteBranches', return_value=['origin/main'])
def testFetchUpstreamTuple_GuessOriginMaster(self, _mockGetRemoteBranches): def testFetchUpstreamTuple_GuessOriginMaster(self, _mockGetRemoteBranches):
self.assertEqual( self.assertEqual(('origin', 'refs/heads/main'),
('origin', 'refs/heads/master'), scm.GIT.FetchUpstreamTuple(self.cwd)) scm.GIT.FetchUpstreamTuple(self.cwd))
@mock.patch('scm.GIT.GetRemoteBranches', @mock.patch('scm.GIT.GetRemoteBranches',
return_value=['origin/master', 'origin/main']) return_value=['origin/master', 'origin/main'])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment