Add retry on git push failure

This adds retry if target is old default branch. This may happen only if
user didn't fetch remote for extended period of time and old default
branch no longer exists.

R=ehmaldonado@chromium.org

Change-Id: I81981049ee006f68a073718180b454c230d055e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/2473757
Commit-Queue: Josip Sokcevic <sokcevic@google.com>
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
changes/57/2473757/8
Josip Sokcevic 5 years ago committed by LUCI CQ
parent 792630c498
commit f736cabba5

@ -926,6 +926,12 @@ def GetGerritBranch(host, project, branch):
raise GerritError(200, 'Unable to get gerrit branch') raise GerritError(200, 'Unable to get gerrit branch')
def GetProjectHead(host, project):
conn = CreateHttpConn(host,
'/projects/%s/HEAD' % urllib.parse.quote(project, ''))
return ReadHttpJsonResponse(conn, accept_statuses=[200])
def GetAccountDetails(host, account_id='self'): def GetAccountDetails(host, account_id='self'):
"""Returns details of the account. """Returns details of the account.

@ -145,6 +145,10 @@ assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values' set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
class GitPushError(Exception):
pass
def DieWithError(message, change_desc=None): def DieWithError(message, change_desc=None):
if change_desc: if change_desc:
SaveDescriptionBackup(change_desc) SaveDescriptionBackup(change_desc)
@ -2080,8 +2084,7 @@ class Changelist(object):
gclient_utils.rmtree(git_info_dir) gclient_utils.rmtree(git_info_dir)
def _RunGitPushWithTraces( def _RunGitPushWithTraces(self, refspec, refspec_opts, git_push_metadata):
self, change_desc, refspec, refspec_opts, git_push_metadata):
"""Run git push and collect the traces resulting from the execution.""" """Run git push and collect the traces resulting from the execution."""
# Create a temporary directory to store traces in. Traces will be compressed # Create a temporary directory to store traces in. Traces will be compressed
# and stored in a 'traces' dir inside depot_tools. # and stored in a 'traces' dir inside depot_tools.
@ -2111,20 +2114,19 @@ class Changelist(object):
push_stdout = push_stdout.decode('utf-8', 'replace') push_stdout = push_stdout.decode('utf-8', 'replace')
except subprocess2.CalledProcessError as e: except subprocess2.CalledProcessError as e:
push_returncode = e.returncode push_returncode = e.returncode
DieWithError('Failed to create a change. Please examine output above ' raise GitPushError(
'for the reason of the failure.\n' 'Failed to create a change. Please examine output above for the '
'Hint: run command below to diagnose common Git/Gerrit ' 'reason of the failure.\n'
'credential problems:\n' 'Hint: run command below to diagnose common Git/Gerrit '
' git cl creds-check\n' 'credential problems:\n'
'\n' ' git cl creds-check\n'
'If git-cl is not working correctly, file a bug under the ' '\n'
'Infra>SDK component including the files below.\n' 'If git-cl is not working correctly, file a bug under the Infra>SDK '
'Review the files before upload, since they might contain ' 'component including the files below.\n'
'sensitive information.\n' 'Review the files before upload, since they might contain sensitive '
'Set the Restrict-View-Google label so that they are not ' 'information.\n'
'publicly accessible.\n' 'Set the Restrict-View-Google label so that they are not publicly '
+ TRACES_MESSAGE % {'trace_name': trace_name}, 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
change_desc)
finally: finally:
execution_time = time_time() - before_push execution_time = time_time() - before_push
metrics.collector.add_repeated('sub_commands', { metrics.collector.add_repeated('sub_commands', {
@ -2143,8 +2145,32 @@ class Changelist(object):
return push_stdout return push_stdout
def CMDUploadChange( def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
self, options, git_diff_args, custom_cl_base, change_desc): change_desc):
"""Upload the current branch to Gerrit, retry if new remote HEAD is
found. options and change_desc may be mutated."""
try:
return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
change_desc)
except GitPushError as e:
remote, remote_branch = self.GetRemoteBranch()
should_retry = remote_branch == DEFAULT_OLD_BRANCH and \
gerrit_util.GetProjectHead(
self._gerrit_host, self._GetGerritProject()) == 'refs/heads/main'
if not should_retry:
DieWithError(str(e), change_desc)
print("WARNING: Detected HEAD change in upstream, fetching remote state")
RunGit(['fetch', remote])
options.edit_description = False
options.force = True
try:
self._CMDUploadChange(options, git_diff_args, custom_cl_base, change_desc)
except GitPushError as e:
DieWithError(str(e), change_desc)
def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
change_desc):
"""Upload the current branch to Gerrit.""" """Upload the current branch to Gerrit."""
remote, remote_branch = self.GetRemoteBranch() remote, remote_branch = self.GetRemoteBranch()
branch = GetTargetRef(remote, remote_branch, options.target_branch) branch = GetTargetRef(remote, remote_branch, options.target_branch)
@ -2242,10 +2268,13 @@ class Changelist(object):
# TODO(tandrii): options.message should be posted as a comment # TODO(tandrii): options.message should be posted as a comment
# if --send-mail is set on non-initial upload as Rietveld used to do it. # if --send-mail is set on non-initial upload as Rietveld used to do it.
title = self._GetTitleForUpload(options) # Set options.title in case user was prompted in _GetTitleForUpload and
if title: # _CMDUploadChange needs to be called again.
options.title = self._GetTitleForUpload(options)
if options.title:
# Punctuation and whitespace in |title| must be percent-encoded. # Punctuation and whitespace in |title| must be percent-encoded.
refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title)) refspec_opts.append(
'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
if options.private: if options.private:
refspec_opts.append('private') refspec_opts.append('private')
@ -2298,12 +2327,12 @@ class Changelist(object):
git_push_metadata = { git_push_metadata = {
'gerrit_host': self._GetGerritHost(), 'gerrit_host': self._GetGerritHost(),
'title': title or '<untitled>', 'title': options.title or '<untitled>',
'change_id': change_id, 'change_id': change_id,
'description': change_desc.description, 'description': change_desc.description,
} }
push_stdout = self._RunGitPushWithTraces( push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
change_desc, refspec, refspec_opts, git_push_metadata) git_push_metadata)
if options.squash: if options.squash:
regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*') regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')

@ -226,6 +226,70 @@ class TestGitClBasic(unittest.TestCase):
actual = set(git_cl.get_cl_statuses(changes, True)) actual = set(git_cl.get_cl_statuses(changes, True))
self.assertEqual(set(zip(changes, statuses)), actual) self.assertEqual(set(zip(changes, statuses)), actual)
def test_upload_to_non_default_branch_no_retry(self):
m = mock.patch('git_cl.Changelist._CMDUploadChange',
side_effect=[git_cl.GitPushError(), None]).start()
mock.patch('git_cl.Changelist.GetRemoteBranch',
return_value=('foo', 'bar')).start()
mock.patch('git_cl.Changelist._GetGerritProject',
return_value='foo').start()
mock.patch('git_cl.gerrit_util.GetProjectHead',
return_value='refs/heads/main').start()
cl = git_cl.Changelist()
options = optparse.Values()
with self.assertRaises(SystemExitMock):
cl.CMDUploadChange(options, [], 'foo', git_cl.ChangeDescription('bar'))
# ensure upload is called once
self.assertEqual(len(m.mock_calls), 1)
sys.exit.assert_called_once_with(1)
# option not set as retry didn't happen
self.assertFalse(hasattr(options, 'force'))
self.assertFalse(hasattr(options, 'edit_description'))
def test_upload_to_old_default_still_active(self):
m = mock.patch('git_cl.Changelist._CMDUploadChange',
side_effect=[git_cl.GitPushError(), None]).start()
mock.patch('git_cl.Changelist.GetRemoteBranch',
return_value=('foo', git_cl.DEFAULT_OLD_BRANCH)).start()
mock.patch('git_cl.Changelist._GetGerritProject',
return_value='foo').start()
mock.patch('git_cl.gerrit_util.GetProjectHead',
return_value='refs/heads/old_default').start()
cl = git_cl.Changelist()
options = optparse.Values()
with self.assertRaises(SystemExitMock):
cl.CMDUploadChange(options, [], 'foo', git_cl.ChangeDescription('bar'))
# ensure upload is called once
self.assertEqual(len(m.mock_calls), 1)
sys.exit.assert_called_once_with(1)
# option not set as retry didn't happen
self.assertFalse(hasattr(options, 'force'))
self.assertFalse(hasattr(options, 'edit_description'))
def test_upload_to_old_default_retry_on_failure(self):
m = mock.patch('git_cl.Changelist._CMDUploadChange',
side_effect=[git_cl.GitPushError(), None]).start()
mock.patch('git_cl.Changelist.GetRemoteBranch',
return_value=('foo', git_cl.DEFAULT_OLD_BRANCH)).start()
mock.patch('git_cl.Changelist._GetGerritProject',
return_value='foo').start()
mock.patch('git_cl.gerrit_util.GetProjectHead',
return_value='refs/heads/main').start()
mock.patch('git_cl.RunGit').start()
cl = git_cl.Changelist()
options = optparse.Values()
cl.CMDUploadChange(options, [], 'foo', git_cl.ChangeDescription('bar'))
# ensure upload is called twice
self.assertEqual(len(m.mock_calls), 2)
# option overrides on retry
self.assertEqual(options.force, True)
self.assertEqual(options.edit_description, False)
def test_get_cl_statuses_no_changes(self): def test_get_cl_statuses_no_changes(self):
self.assertEqual([], list(git_cl.get_cl_statuses([], True))) self.assertEqual([], list(git_cl.get_cl_statuses([], True)))

Loading…
Cancel
Save