blob: e53e8afdce5f78e98819b1a5065782af542be333 [file] [log] [blame]
Edward Lemur1f3bafb2019-10-08 17:56:33 +00001#!/usr/bin/env vpython
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010016import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000017import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000018import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010019import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000022import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import optparse
24import os
25import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010026import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000027import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070029import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000031import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000036from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000037import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000038import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000039import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000040import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000041import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000042import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000043import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000044import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000045import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000046import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000047import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000048import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000049import presubmit_support
50import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000051import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040052import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import watchlists
56
Edward Lemur79d4f992019-11-11 23:49:02 +000057from third_party import six
58from six.moves import urllib
59
60
61if sys.version_info.major == 3:
62 basestring = (str,) # pylint: disable=redefined-builtin
63
Edward Lemurb9830242019-10-30 22:19:20 +000064
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
Edward Lemur0f58ae42019-04-30 17:24:12 +000067# Traces for git push will be stored in a traces directory inside the
68# depot_tools checkout.
69DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
70TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
71
72# When collecting traces, Git hashes will be reduced to 6 characters to reduce
73# the size after compression.
74GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
75# Used to redact the cookies from the gitcookies file.
76GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
77
Edward Lemurd4d1ba42019-09-20 21:46:37 +000078MAX_ATTEMPTS = 3
79
Edward Lemur1b52d872019-05-09 21:12:12 +000080# The maximum number of traces we will keep. Multiplied by 3 since we store
81# 3 files per trace.
82MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000083# Message to be displayed to the user to inform where to find the traces for a
84# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000085TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000086'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000087'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000088' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000089'Copies of your gitcookies file and git config have been recorded at:\n'
90' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000091# Format of the message to be stored as part of the traces to give developers a
92# better context when they go through traces.
93TRACES_README_FORMAT = (
94'Date: %(now)s\n'
95'\n'
96'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
97'Title: %(title)s\n'
98'\n'
99'%(description)s\n'
100'\n'
101'Execution time: %(execution_time)s\n'
102'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000103
tandrii9d2c7a32016-06-22 03:42:45 -0700104COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800105POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000106DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000107REFS_THAT_ALIAS_TO_OTHER_REFS = {
108 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
109 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
110}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
thestig@chromium.org44202a22014-03-11 19:22:18 +0000112# Valid extensions for files we want to lint.
113DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
114DEFAULT_LINT_IGNORE_REGEX = r"$^"
115
Aiden Bennerc08566e2018-10-03 17:52:42 +0000116# File name for yapf style config files.
117YAPF_CONFIG_FILENAME = '.style.yapf'
118
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000119# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000120Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000121
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000122# Initialized in main()
123settings = None
124
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100125# Used by tests/git_cl_test.py to add extra logging.
126# Inside the weirdly failing test, add this:
127# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700128# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100129_IS_BEING_TESTED = False
130
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000131
Christopher Lamf732cd52017-01-24 12:40:11 +1100132def DieWithError(message, change_desc=None):
133 if change_desc:
134 SaveDescriptionBackup(change_desc)
135
vapiera7fbd5a2016-06-16 09:17:49 -0700136 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000137 sys.exit(1)
138
139
Christopher Lamf732cd52017-01-24 12:40:11 +1100140def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000141 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000142 print('\nsaving CL description to %s\n' % backup_path)
Josip906bfde2020-01-31 22:38:49 +0000143 with open(backup_path, 'w') as backup_file:
144 backup_file.write(change_desc.description)
Christopher Lamf732cd52017-01-24 12:40:11 +1100145
146
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000147def GetNoGitPagerEnv():
148 env = os.environ.copy()
149 # 'cat' is a magical git string that disables pagers on all platforms.
150 env['GIT_PAGER'] = 'cat'
151 return env
152
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000153
bsep@chromium.org627d9002016-04-29 00:00:52 +0000154def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000156 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
157 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000158 except subprocess2.CalledProcessError as e:
159 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000160 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000162 'Command "%s" failed.\n%s' % (
163 ' '.join(args), error_message or e.stdout or ''))
Edward Lemur79d4f992019-11-11 23:49:02 +0000164 return e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000165
166
167def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000168 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000169 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000170
171
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000172def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000173 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700174 if suppress_stderr:
175 stderr = subprocess2.VOID
176 else:
177 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000178 try:
tandrii5d48c322016-08-18 16:19:37 -0700179 (out, _), code = subprocess2.communicate(['git'] + args,
180 env=GetNoGitPagerEnv(),
181 stdout=subprocess2.PIPE,
182 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000183 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700184 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900185 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000186 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000187
188
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000189def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000190 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000191 return RunGitWithCode(args, suppress_stderr=True)[1]
192
193
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000194def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000195 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000196 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000197 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000198 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000199
200
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000201def BranchExists(branch):
202 """Return True if specified branch exists."""
203 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
204 suppress_stderr=True)
205 return not code
206
207
tandrii2a16b952016-10-19 07:09:44 -0700208def time_sleep(seconds):
209 # Use this so that it can be mocked in tests without interfering with python
210 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700211 return time.sleep(seconds)
212
213
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000214def time_time():
215 # Use this so that it can be mocked in tests without interfering with python
216 # system machinery.
217 return time.time()
218
219
Edward Lemur1b52d872019-05-09 21:12:12 +0000220def datetime_now():
221 # Use this so that it can be mocked in tests without interfering with python
222 # system machinery.
223 return datetime.datetime.now()
224
225
maruel@chromium.org90541732011-04-01 17:54:18 +0000226def ask_for_data(prompt):
227 try:
228 return raw_input(prompt)
229 except KeyboardInterrupt:
230 # Hide the exception.
231 sys.exit(1)
232
233
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100234def confirm_or_exit(prefix='', action='confirm'):
235 """Asks user to press enter to continue or press Ctrl+C to abort."""
236 if not prefix or prefix.endswith('\n'):
237 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100238 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100239 mid = ' Press'
240 elif prefix.endswith(' '):
241 mid = 'press'
242 else:
243 mid = ' press'
244 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
245
246
247def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000248 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100249 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
250 while True:
251 if 'yes'.startswith(result):
252 return True
253 if 'no'.startswith(result):
254 return False
255 result = ask_for_data('Please, type yes or no: ').lower()
256
257
tandrii5d48c322016-08-18 16:19:37 -0700258def _git_branch_config_key(branch, key):
259 """Helper method to return Git config key for a branch."""
260 assert branch, 'branch name is required to set git config for it'
261 return 'branch.%s.%s' % (branch, key)
262
263
264def _git_get_branch_config_value(key, default=None, value_type=str,
265 branch=False):
266 """Returns git config value of given or current branch if any.
267
268 Returns default in all other cases.
269 """
270 assert value_type in (int, str, bool)
271 if branch is False: # Distinguishing default arg value from None.
272 branch = GetCurrentBranch()
273
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000274 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700275 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000276
tandrii5d48c322016-08-18 16:19:37 -0700277 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700278 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700279 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000280 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700281 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700282 args.append(_git_branch_config_key(branch, key))
283 code, out = RunGitWithCode(args)
284 if code == 0:
285 value = out.strip()
286 if value_type == int:
287 return int(value)
288 if value_type == bool:
289 return bool(value.lower() == 'true')
290 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000291 return default
292
293
tandrii5d48c322016-08-18 16:19:37 -0700294def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000295 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700296
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000297 If value is None, the key will be unset, otherwise it will be set.
298 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700299 """
300 if not branch:
301 branch = GetCurrentBranch()
302 assert branch, 'a branch name OR currently checked out branch is required'
303 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700304 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700305 if value is None:
306 args.append('--unset')
307 elif isinstance(value, bool):
308 args.append('--bool')
309 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700310 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000311 # `git config` also has --int, but apparently git config suffers from
312 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700313 value = str(value)
314 args.append(_git_branch_config_key(branch, key))
315 if value is not None:
316 args.append(value)
317 RunGit(args, **kwargs)
318
319
machenbach@chromium.org45453142015-09-15 08:45:22 +0000320def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000321 prop_list = getattr(options, 'properties', [])
322 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000323 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000324 try:
325 properties[key] = json.loads(val)
326 except ValueError:
327 pass # If a value couldn't be evaluated, treat it as a string.
328 return properties
329
330
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000331# TODO(crbug.com/976104): Remove this function once git-cl try-results has
332# migrated to use buildbucket v2
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700354 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000355 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000356 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700357 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000358 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 content)
360 return content_json
361 if response.status < 500 or try_count >= 2:
362 raise httplib2.HttpLib2Error(content)
363
364 # status >= 500 means transient failures.
365 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000366 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367 try_count += 1
368 assert False, 'unreachable'
369
370
Edward Lemur4c707a22019-09-24 21:13:43 +0000371def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000372 """Calls a buildbucket v2 method and returns the parsed json response."""
373 headers = {
374 'Accept': 'application/json',
375 'Content-Type': 'application/json',
376 }
377 request = json.dumps(request)
378 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
379
380 logging.info('POST %s with %s' % (url, request))
381
382 attempts = 1
383 time_to_sleep = 1
384 while True:
385 response, content = http.request(url, 'POST', body=request, headers=headers)
386 if response.status == 200:
387 return json.loads(content[4:])
388 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
389 msg = '%s error when calling POST %s with %s: %s' % (
390 response.status, url, request, content)
391 raise BuildbucketResponseException(msg)
392 logging.debug(
393 '%s error when calling POST %s with %s. '
394 'Sleeping for %d seconds and retrying...' % (
395 response.status, url, request, time_to_sleep))
396 time.sleep(time_to_sleep)
397 time_to_sleep *= 2
398 attempts += 1
399
400 assert False, 'unreachable'
401
402
qyearsley1fdfcb62016-10-24 13:22:03 -0700403def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700404 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000405 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700406 """
qyearsleydd49f942016-10-28 11:57:22 -0700407 # If no bots are listed, we try to get a set of builders and tests based
408 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 if not options.bot:
Edward Lemur3b8094a2020-02-07 22:34:43 +0000410 change = changelist.GetChange(changelist.GetCommonAncestorWithUpstream())
qyearsley136b49f2016-10-31 09:02:26 -0700411 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700412 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 change=change,
414 changed_files=change.LocalPaths(),
415 repository_root=settings.GetRoot(),
416 default_presubmit=None,
417 project=None,
418 verbose=options.verbose,
419 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700420 if masters is None:
421 return None
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000422 return {m: b for m, b in masters.items()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700423
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 if options.bucket:
425 return {options.bucket: {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000426 option_parser.error(
Edward Lemur5ef16a32019-11-11 21:13:25 +0000427 'Please specify the bucket, e.g. "-B chromium/try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700428
429
Edward Lemur6215c792019-10-03 21:59:05 +0000430def _parse_bucket(raw_bucket):
431 legacy = True
432 project = bucket = None
433 if '/' in raw_bucket:
434 legacy = False
435 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000436 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000437 elif raw_bucket.startswith('luci.'):
438 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000439 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000440 elif '.' in raw_bucket:
441 project = raw_bucket.split('.')[0]
442 bucket = raw_bucket
443 # Legacy buckets.
444 if legacy:
445 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
446 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000447
448
Edward Lemur5b929a42019-10-21 17:57:39 +0000449def _trigger_try_jobs(changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000450 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700451
452 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000453 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700454 buckets: A nested dict mapping bucket names to builders to tests.
455 options: Command-line options.
456 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000457 print('Scheduling jobs on:')
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000458 for bucket, builders_and_tests in sorted(buckets.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000459 print('Bucket:', bucket)
460 print('\n'.join(
461 ' %s: %s' % (builder, tests)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000462 for builder, tests in sorted(builders_and_tests.items())))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000463 print('To see results here, run: git cl try-results')
464 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700465
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000466 requests = _make_try_job_schedule_requests(
467 changelist, buckets, options, patchset)
468 if not requests:
469 return
470
Edward Lemur5b929a42019-10-21 17:57:39 +0000471 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000472 http.force_exception_to_status_code = True
473
474 batch_request = {'requests': requests}
475 batch_response = _call_buildbucket(
476 http, options.buildbucket_host, 'Batch', batch_request)
477
478 errors = [
479 ' ' + response['error']['message']
480 for response in batch_response.get('responses', [])
481 if 'error' in response
482 ]
483 if errors:
484 raise BuildbucketResponseException(
485 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
486
487
488def _make_try_job_schedule_requests(changelist, buckets, options, patchset):
Edward Lemurf0faf482019-09-25 20:40:17 +0000489 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000490 shared_properties = {'category': getattr(options, 'category', 'git_cl_try')}
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000491 if getattr(options, 'clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000492 shared_properties['clobber'] = True
493 shared_properties.update(_get_properties_from_options(options) or {})
494
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000495 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
496 if options.retry_failed:
497 shared_tags.append({'key': 'retry_failed',
498 'value': '1'})
499
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000500 requests = []
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000501 for raw_bucket, builders_and_tests in sorted(buckets.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000502 project, bucket = _parse_bucket(raw_bucket)
503 if not project or not bucket:
504 print('WARNING Could not parse bucket "%s". Skipping.' % raw_bucket)
505 continue
506
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000507 for builder, tests in sorted(builders_and_tests.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000508 properties = shared_properties.copy()
509 if 'presubmit' in builder.lower():
510 properties['dry_run'] = 'true'
511 if tests:
512 properties['testfilter'] = tests
513
514 requests.append({
515 'scheduleBuild': {
516 'requestId': str(uuid.uuid4()),
517 'builder': {
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000518 'project': getattr(options, 'project', None) or project,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000519 'bucket': bucket,
520 'builder': builder,
521 },
522 'gerritChanges': gerrit_changes,
523 'properties': properties,
524 'tags': [
525 {'key': 'builder', 'value': builder},
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000526 ] + shared_tags,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000527 }
528 })
Anthony Polito1a5fe232020-01-24 23:17:52 +0000529
530 if options.revision:
531 requests[-1]['scheduleBuild']['gitilesCommit'] = {
532 'host': gerrit_changes[0]['host'],
533 'project': gerrit_changes[0]['project'],
534 'id': options.revision
535 }
536
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000537 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000538
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000539
Edward Lemur5b929a42019-10-21 17:57:39 +0000540def fetch_try_jobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000541 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000543 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000545 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000546 request = {
547 'predicate': {
548 'gerritChanges': [changelist.GetGerritChange(patchset)],
549 },
550 'fields': ','.join('builds.*.' + field for field in fields),
551 }
tandrii221ab252016-10-06 08:12:04 -0700552
Edward Lemur5b929a42019-10-21 17:57:39 +0000553 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000554 if authenticator.has_cached_credentials():
555 http = authenticator.authorize(httplib2.Http())
556 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700557 print('Warning: Some results might be missing because %s' %
558 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000559 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 http.force_exception_to_status_code = True
562
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000563 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
564 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565
Edward Lemur5b929a42019-10-21 17:57:39 +0000566def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000567 """Fetches builds from the latest patchset that has builds (within
568 the last few patchsets).
569
570 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000571 changelist (Changelist): The CL to fetch builds for
572 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000573 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
574 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000575 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000576 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
577 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000578 """
579 assert buildbucket_host
580 assert changelist.GetIssue(), 'CL must be uploaded first'
581 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000582 if latest_patchset is None:
583 assert changelist.GetMostRecentPatchset()
584 ps = changelist.GetMostRecentPatchset()
585 else:
586 assert latest_patchset > 0, latest_patchset
587 ps = latest_patchset
588
Quinten Yearsley983111f2019-09-26 17:18:48 +0000589 min_ps = max(1, ps - 5)
590 while ps >= min_ps:
Edward Lemur5b929a42019-10-21 17:57:39 +0000591 builds = fetch_try_jobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000592 if len(builds):
593 return builds, ps
594 ps -= 1
595 return [], 0
596
597
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000598def _filter_failed_for_retry(all_builds):
599 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000600
601 Args:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000602 all_builds (list): Builds, in the format returned by fetch_try_jobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000603 i.e. a list of buildbucket.v2.Builds which includes status and builder
604 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000605
606 Returns:
607 A dict of bucket to builder to tests (empty list). This is the same format
608 accepted by _trigger_try_jobs and returned by _get_bucket_map.
609 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000610
611 def _builder_of(build):
612 builder = build['builder']
613 return (builder['project'], builder['bucket'], builder['builder'])
614
615 res = collections.defaultdict(dict)
616 ordered = sorted(all_builds, key=lambda b: (_builder_of(b), b['createTime']))
617 for (proj, buck, bldr), builds in itertools.groupby(ordered, key=_builder_of):
618 # If builder had several builds, retry only if the last one failed.
619 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
620 # build, but in case of retrying failed jobs retrying a flaky one makes
621 # sense.
622 builds = list(builds)
623 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
624 continue
625 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
626 for t in builds[-1]['tags']):
627 # Don't retry experimental build previously triggered by CQ.
628 continue
629 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
630 # Don't retry if any are running.
631 continue
632 res[proj + '/' + buck][bldr] = []
633 return res
Quinten Yearsley983111f2019-09-26 17:18:48 +0000634
635
qyearsleyeab3c042016-08-24 09:18:28 -0700636def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637 """Prints nicely result of fetch_try_jobs."""
638 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000639 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640 return
641
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000642 longest_builder = max(len(b['builder']['builder']) for b in builds)
643 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000644 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000645 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
646 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000647
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000648 builds_by_status = {}
649 for b in builds:
650 builds_by_status.setdefault(b['status'], []).append({
651 'id': b['id'],
652 'name': name_fmt.format(
653 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
654 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000655
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000656 sort_key = lambda b: (b['name'], b['id'])
657
658 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000659 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000660 if not builds:
661 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000662
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000663 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000664 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000665 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666 else:
667 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
668
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000669 print(colorize(title))
670 for b in sorted(builds, key=sort_key):
671 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000674 print_builds(
675 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
676 print_builds(
677 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
678 color=Fore.MAGENTA)
679 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
680 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
681 color=Fore.MAGENTA)
682 print_builds('Started:', builds_by_status.pop('STARTED', []))
683 print_builds(
684 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000685 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000686 print_builds(
687 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000688 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000689
690
Aiden Bennerc08566e2018-10-03 17:52:42 +0000691def _ComputeDiffLineRanges(files, upstream_commit):
692 """Gets the changed line ranges for each file since upstream_commit.
693
694 Parses a git diff on provided files and returns a dict that maps a file name
695 to an ordered list of range tuples in the form (start_line, count).
696 Ranges are in the same format as a git diff.
697 """
698 # If files is empty then diff_output will be a full diff.
699 if len(files) == 0:
700 return {}
701
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000702 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000703 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000704 diff_output = RunGit(diff_cmd)
705
706 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
707 # 2 capture groups
708 # 0 == fname of diff file
709 # 1 == 'diff_start,diff_count' or 'diff_start'
710 # will match each of
711 # diff --git a/foo.foo b/foo.py
712 # @@ -12,2 +14,3 @@
713 # @@ -12,2 +17 @@
714 # running re.findall on the above string with pattern will give
715 # [('foo.py', ''), ('', '14,3'), ('', '17')]
716
717 curr_file = None
718 line_diffs = {}
719 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
720 if match[0] != '':
721 # Will match the second filename in diff --git a/a.py b/b.py.
722 curr_file = match[0]
723 line_diffs[curr_file] = []
724 else:
725 # Matches +14,3
726 if ',' in match[1]:
727 diff_start, diff_count = match[1].split(',')
728 else:
729 # Single line changes are of the form +12 instead of +12,1.
730 diff_start = match[1]
731 diff_count = 1
732
733 diff_start = int(diff_start)
734 diff_count = int(diff_count)
735
736 # If diff_count == 0 this is a removal we can ignore.
737 line_diffs[curr_file].append((diff_start, diff_count))
738
739 return line_diffs
740
741
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000742def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000743 """Checks if a yapf file is in any parent directory of fpath until top_dir.
744
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000745 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000746 is found returns None. Uses yapf_config_cache as a cache for previously found
747 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000748 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000749 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000750 # Return result if we've already computed it.
751 if fpath in yapf_config_cache:
752 return yapf_config_cache[fpath]
753
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000754 parent_dir = os.path.dirname(fpath)
755 if os.path.isfile(fpath):
756 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000757 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000758 # Otherwise fpath is a directory
759 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
760 if os.path.isfile(yapf_file):
761 ret = yapf_file
762 elif fpath == top_dir or parent_dir == fpath:
763 # If we're at the top level directory, or if we're at root
764 # there is no provided style.
765 ret = None
766 else:
767 # Otherwise recurse on the current directory.
768 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000769 yapf_config_cache[fpath] = ret
770 return ret
771
772
Brian Sheedyb4307d52019-12-02 19:18:17 +0000773def _GetYapfIgnorePatterns(top_dir):
774 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000775
776 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
777 but this functionality appears to break when explicitly passing files to
778 yapf for formatting. According to
779 https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
780 the .yapfignore file should be in the directory that yapf is invoked from,
781 which we assume to be the top level directory in this case.
782
783 Args:
784 top_dir: The top level directory for the repository being formatted.
785
786 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000787 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000788 """
789 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000790 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000791 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000792 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000793
Brian Sheedyb4307d52019-12-02 19:18:17 +0000794 with open(yapfignore_file) as f:
795 for line in f.readlines():
796 stripped_line = line.strip()
797 # Comments and blank lines should be ignored.
798 if stripped_line.startswith('#') or stripped_line == '':
799 continue
800 ignore_patterns.add(stripped_line)
801 return ignore_patterns
802
803
804def _FilterYapfIgnoredFiles(filepaths, patterns):
805 """Filters out any filepaths that match any of the given patterns.
806
807 Args:
808 filepaths: An iterable of strings containing filepaths to filter.
809 patterns: An iterable of strings containing fnmatch patterns to filter on.
810
811 Returns:
812 A list of strings containing all the elements of |filepaths| that did not
813 match any of the patterns in |patterns|.
814 """
815 # Not inlined so that tests can use the same implementation.
816 return [f for f in filepaths
817 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000818
819
Aaron Gable13101a62018-02-09 13:20:41 -0800820def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000821 """Prints statistics about the change to the user."""
822 # --no-ext-diff is broken in some versions of Git, so try to work around
823 # this by overriding the environment (but there is still a problem if the
824 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000825 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000826 if 'GIT_EXTERNAL_DIFF' in env:
827 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000828
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000829 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800830 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000831 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000832
833
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000834class BuildbucketResponseException(Exception):
835 pass
836
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838class Settings(object):
839 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000841 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 self.tree_status_url = None
843 self.viewvc_url = None
844 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000845 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000846 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000847 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000848 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000849 self.format_full_by_default = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850
851 def LazyUpdateIfNeeded(self):
852 """Updates the settings from a codereview.settings file, if available."""
853 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000854 # The only value that actually changes the behavior is
855 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000856 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000857 error_ok=True
858 ).strip().lower()
859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000861 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862 LoadCodereviewSettingsFromFile(cr_settings_file)
Josip906bfde2020-01-31 22:38:49 +0000863 cr_settings_file.close()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 self.updated = True
865
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000866 @staticmethod
867 def GetRelativeRoot():
868 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000869
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000871 if self.root is None:
872 self.root = os.path.abspath(self.GetRelativeRoot())
873 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 def GetTreeStatusUrl(self, error_ok=False):
876 if not self.tree_status_url:
877 error_message = ('You must configure your tree status URL by running '
878 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000879 self.tree_status_url = self._GetConfig(
880 'rietveld.tree-status-url', error_ok=error_ok,
881 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 return self.tree_status_url
883
884 def GetViewVCUrl(self):
885 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000886 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887 return self.viewvc_url
888
rmistry@google.com90752582014-01-14 21:04:50 +0000889 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000890 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000891
rmistry@google.com5626a922015-02-26 14:03:30 +0000892 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000893 run_post_upload_hook = self._GetConfig(
894 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000895 return run_post_upload_hook == "True"
896
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000897 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000898 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000899
ukai@chromium.orge8077812012-02-03 03:41:46 +0000900 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000901 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700903 self.is_gerrit = (
904 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000905 return self.is_gerrit
906
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000907 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000908 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000909 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700910 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
911 if self.squash_gerrit_uploads is None:
912 # Default is squash now (http://crbug.com/611892#c23).
913 self.squash_gerrit_uploads = not (
914 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
915 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000916 return self.squash_gerrit_uploads
917
tandriia60502f2016-06-20 02:01:53 -0700918 def GetSquashGerritUploadsOverride(self):
919 """Return True or False if codereview.settings should be overridden.
920
921 Returns None if no override has been defined.
922 """
923 # See also http://crbug.com/611892#c23
924 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
925 error_ok=True).strip()
926 if result == 'true':
927 return True
928 if result == 'false':
929 return False
930 return None
931
tandrii@chromium.org28253532016-04-14 13:46:56 +0000932 def GetGerritSkipEnsureAuthenticated(self):
933 """Return True if EnsureAuthenticated should not be done for Gerrit
934 uploads."""
935 if self.gerrit_skip_ensure_authenticated is None:
936 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000937 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000938 error_ok=True).strip() == 'true')
939 return self.gerrit_skip_ensure_authenticated
940
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000941 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000942 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000943 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000944 # Git requires single quotes for paths with spaces. We need to replace
945 # them with double quotes for Windows to treat such paths as a single
946 # path.
947 self.git_editor = self._GetConfig(
948 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000949 return self.git_editor or None
950
thestig@chromium.org44202a22014-03-11 19:22:18 +0000951 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000952 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000953 DEFAULT_LINT_REGEX)
954
955 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000956 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000957 DEFAULT_LINT_IGNORE_REGEX)
958
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000959 def GetFormatFullByDefault(self):
960 if self.format_full_by_default is None:
961 result = (
962 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
963 error_ok=True).strip())
964 self.format_full_by_default = (result == 'true')
965 return self.format_full_by_default
966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 def _GetConfig(self, param, **kwargs):
968 self.LazyUpdateIfNeeded()
969 return RunGit(['config', param], **kwargs).strip()
970
971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972def ShortBranchName(branch):
973 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000974 return branch.replace('refs/heads/', '', 1)
975
976
977def GetCurrentBranchRef():
978 """Returns branch ref (e.g., refs/heads/master) or None."""
979 return RunGit(['symbolic-ref', 'HEAD'],
980 stderr=subprocess2.VOID, error_ok=True).strip() or None
981
982
983def GetCurrentBranch():
984 """Returns current branch or None.
985
986 For refs/heads/* branches, returns just last part. For others, full ref.
987 """
988 branchref = GetCurrentBranchRef()
989 if branchref:
990 return ShortBranchName(branchref)
991 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992
993
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000994class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000995 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000996 NONE = 'none'
997 DRY_RUN = 'dry_run'
998 COMMIT = 'commit'
999
1000 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1001
1002
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001003class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +00001004 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001005 self.issue = issue
1006 self.patchset = patchset
1007 self.hostname = hostname
1008
1009 @property
1010 def valid(self):
1011 return self.issue is not None
1012
1013
Edward Lemurf38bc172019-09-03 21:02:13 +00001014def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001015 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1016 fail_result = _ParsedIssueNumberArgument()
1017
Edward Lemur678a6842019-10-03 22:25:05 +00001018 if isinstance(arg, int):
1019 return _ParsedIssueNumberArgument(issue=arg)
1020 if not isinstance(arg, basestring):
1021 return fail_result
1022
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001023 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00001024 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001025 if not arg.startswith('http'):
1026 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001027
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001028 url = gclient_utils.UpgradeToHttps(arg)
1029 try:
Edward Lemur79d4f992019-11-11 23:49:02 +00001030 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001031 except ValueError:
1032 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001033
Edward Lemur678a6842019-10-03 22:25:05 +00001034 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
1035 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
1036 # Short urls like https://domain/<issue_number> can be used, but don't allow
1037 # specifying the patchset (you'd 404), but we allow that here.
1038 if parsed_url.path == '/':
1039 part = parsed_url.fragment
1040 else:
1041 part = parsed_url.path
1042
1043 match = re.match(
1044 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
1045 if not match:
1046 return fail_result
1047
1048 issue = int(match.group('issue'))
1049 patchset = match.group('patchset')
1050 return _ParsedIssueNumberArgument(
1051 issue=issue,
1052 patchset=int(patchset) if patchset else None,
1053 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001054
1055
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001056def _create_description_from_log(args):
1057 """Pulls out the commit log to use as a base for the CL description."""
1058 log_args = []
1059 if len(args) == 1 and not args[0].endswith('.'):
1060 log_args = [args[0] + '..']
1061 elif len(args) == 1 and args[0].endswith('...'):
1062 log_args = [args[0][:-1]]
1063 elif len(args) == 2:
1064 log_args = [args[0] + '..' + args[1]]
1065 else:
1066 log_args = args[:] # Hope for the best!
1067 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1068
1069
Aaron Gablea45ee112016-11-22 15:14:38 -08001070class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001071 def __init__(self, issue, url):
1072 self.issue = issue
1073 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001074 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001075
1076 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001077 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001078 self.issue, self.url)
1079
1080
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001081_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001082 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001083 # TODO(tandrii): these two aren't known in Gerrit.
1084 'approval', 'disapproval'])
1085
1086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001088 """Changelist works with one changelist in local branch.
1089
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001090 Notes:
1091 * Not safe for concurrent multi-{thread,process} use.
1092 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001093 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001094 """
1095
Edward Lemur125d60a2019-09-13 18:25:41 +00001096 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001097 """Create a new ChangeList instance.
1098
Edward Lemurf38bc172019-09-03 21:02:13 +00001099 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001100 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001102 global settings
1103 if not settings:
1104 # Happens when git_cl.py is used as a utility library.
1105 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107 self.branchref = branchref
1108 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001109 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 self.branch = ShortBranchName(self.branchref)
1111 else:
1112 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001113 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001114 self.lookedup_issue = False
1115 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 self.has_description = False
1117 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001118 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001120 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001121 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001122 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001123 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001124
Edward Lemur125d60a2019-09-13 18:25:41 +00001125 # Lazily cached values.
1126 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1127 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1128 # Map from change number (issue) to its detail cache.
1129 self._detail_cache = {}
1130
1131 if codereview_host is not None:
1132 assert not codereview_host.startswith('https://'), codereview_host
1133 self._gerrit_host = codereview_host
1134 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001135
1136 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001137 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001138
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001139 The return value is a string suitable for passing to git cl with the --cc
1140 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001141 """
1142 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001143 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001144 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1146 return self.cc
1147
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001148 def GetCCListWithoutDefault(self):
1149 """Return the users cc'd on this CL excluding default ones."""
1150 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001151 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 return self.cc
1153
Daniel Cheng7227d212017-11-17 08:12:37 -08001154 def ExtendCC(self, more_cc):
1155 """Extends the list of users to cc on this CL based on the changed files."""
1156 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157
1158 def GetBranch(self):
1159 """Returns the short branch name, e.g. 'master'."""
1160 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001162 if not branchref:
1163 return None
1164 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 self.branch = ShortBranchName(self.branchref)
1166 return self.branch
1167
1168 def GetBranchRef(self):
1169 """Returns the full branch name, e.g. 'refs/heads/master'."""
1170 self.GetBranch() # Poke the lazy loader.
1171 return self.branchref
1172
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001173 def ClearBranch(self):
1174 """Clears cached branch data of this object."""
1175 self.branch = self.branchref = None
1176
tandrii5d48c322016-08-18 16:19:37 -07001177 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1178 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1179 kwargs['branch'] = self.GetBranch()
1180 return _git_get_branch_config_value(key, default, **kwargs)
1181
1182 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1183 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1184 assert self.GetBranch(), (
1185 'this CL must have an associated branch to %sset %s%s' %
1186 ('un' if value is None else '',
1187 key,
1188 '' if value is None else ' to %r' % value))
1189 kwargs['branch'] = self.GetBranch()
1190 return _git_set_branch_config_value(key, value, **kwargs)
1191
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001192 @staticmethod
1193 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001194 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 e.g. 'origin', 'refs/heads/master'
1196 """
1197 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001198 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001201 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001203 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1204 error_ok=True).strip()
1205 if upstream_branch:
1206 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001208 # Else, try to guess the origin remote.
1209 remote_branches = RunGit(['branch', '-r']).split()
1210 if 'origin/master' in remote_branches:
1211 # Fall back on origin/master if it exits.
1212 remote = 'origin'
1213 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001215 DieWithError(
1216 'Unable to determine default branch to diff against.\n'
1217 'Either pass complete "git diff"-style arguments, like\n'
1218 ' git cl upload origin/master\n'
1219 'or verify this branch is set up to track another \n'
1220 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221
1222 return remote, upstream_branch
1223
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001224 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001225 upstream_branch = self.GetUpstreamBranch()
1226 if not BranchExists(upstream_branch):
1227 DieWithError('The upstream for the current branch (%s) does not exist '
1228 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001229 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001230 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 def GetUpstreamBranch(self):
1233 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001234 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001235 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001236 upstream_branch = upstream_branch.replace('refs/heads/',
1237 'refs/remotes/%s/' % remote)
1238 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1239 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 self.upstream_branch = upstream_branch
1241 return self.upstream_branch
1242
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001243 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001244 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 remote, branch = None, self.GetBranch()
1246 seen_branches = set()
1247 while branch not in seen_branches:
1248 seen_branches.add(branch)
1249 remote, branch = self.FetchUpstreamTuple(branch)
1250 branch = ShortBranchName(branch)
1251 if remote != '.' or branch.startswith('refs/remotes'):
1252 break
1253 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001254 remotes = RunGit(['remote'], error_ok=True).split()
1255 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001256 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001257 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001259 logging.warn('Could not determine which remote this change is '
1260 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001261 else:
1262 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001263 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 branch = 'HEAD'
1265 if branch.startswith('refs/remotes'):
1266 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001267 elif branch.startswith('refs/branch-heads/'):
1268 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 else:
1270 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001271 return self._remote
1272
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 def GitSanityChecks(self, upstream_git_obj):
1274 """Checks git repo status and ensures diff is from local commits."""
1275
sbc@chromium.org79706062015-01-14 21:18:12 +00001276 if upstream_git_obj is None:
1277 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001278 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001279 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001280 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001281 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001282 return False
1283
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 # Verify the commit we're diffing against is in our current branch.
1285 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1286 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1287 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001288 print('ERROR: %s is not in the current branch. You may need to rebase '
1289 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 return False
1291
1292 # List the commits inside the diff, and verify they are all local.
1293 commits_in_diff = RunGit(
1294 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1295 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1296 remote_branch = remote_branch.strip()
1297 if code != 0:
1298 _, remote_branch = self.GetRemoteBranch()
1299
1300 commits_in_remote = RunGit(
1301 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1302
1303 common_commits = set(commits_in_diff) & set(commits_in_remote)
1304 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001305 print('ERROR: Your diff contains %d commits already in %s.\n'
1306 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1307 'the diff. If you are using a custom git flow, you can override'
1308 ' the reference used for this check with "git config '
1309 'gitcl.remotebranch <git-ref>".' % (
1310 len(common_commits), remote_branch, upstream_git_obj),
1311 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 return False
1313 return True
1314
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001315 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001316 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001317
1318 Returns None if it is not set.
1319 """
tandrii5d48c322016-08-18 16:19:37 -07001320 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 def GetRemoteUrl(self):
1323 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1324
1325 Returns None if there is no remote.
1326 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001327 is_cached, value = self._cached_remote_url
1328 if is_cached:
1329 return value
1330
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001332 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1333
Edward Lemur298f2cf2019-02-22 21:40:39 +00001334 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001335 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001336 if host:
1337 self._cached_remote_url = (True, url)
1338 return url
1339
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001340 # If it cannot be parsed as an url, assume it is a local directory,
1341 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001342 logging.warning('"%s" doesn\'t appear to point to a git host. '
1343 'Interpreting it as a local directory.', url)
1344 if not os.path.isdir(url):
1345 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001346 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1347 'but it doesn\'t exist.',
1348 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001349 return None
1350
1351 cache_path = url
1352 url = RunGit(['config', 'remote.%s.url' % remote],
1353 error_ok=True,
1354 cwd=url).strip()
1355
Edward Lemur79d4f992019-11-11 23:49:02 +00001356 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001357 if not host:
1358 logging.error(
1359 'Remote "%(remote)s" for branch "%(branch)s" points to '
1360 '"%(cache_path)s", but it is misconfigured.\n'
1361 '"%(cache_path)s" must be a git repo and must have a remote named '
1362 '"%(remote)s" pointing to the git host.', {
1363 'remote': remote,
1364 'cache_path': cache_path,
1365 'branch': self.GetBranch()})
1366 return None
1367
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001368 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001369 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001371 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001372 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001373 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001374 self.issue = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001375 self.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001376 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 return self.issue
1378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 def GetIssueURL(self):
1380 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001381 issue = self.GetIssue()
1382 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001383 return None
Edward Lemur125d60a2019-09-13 18:25:41 +00001384 return '%s/%s' % (self.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
Edward Lemur7f6dec02020-02-06 20:23:58 +00001386 def GetLocalDescription(self, upstream_branch):
1387 """Return the log messages of all commits up to the branch point."""
1388 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1389 return RunGitWithCode(args)[1].strip()
1390
Edward Lemur6c6827c2020-02-06 21:15:18 +00001391 def FetchDescription(self, pretty=False):
1392 assert self.GetIssue(), 'issue is required to query Gerrit'
1393
1394 if not self.has_description:
1395 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1396 current_rev = data['current_revision']
1397 self.description = data['revisions'][current_rev]['commit']['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 self.has_description = True
Edward Lemur6c6827c2020-02-06 21:15:18 +00001399
1400 if not pretty:
1401 return self.description
1402
1403 # Set width to 72 columns + 2 space indent.
1404 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1405 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1406 lines = self.description.splitlines()
1407 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408
1409 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001410 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001411 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001412 self.patchset = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001413 self.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 return self.patchset
1416
1417 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001418 """Set this branch's patchset. If patchset=0, clears the patchset."""
1419 assert self.GetBranch()
1420 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001421 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001422 else:
1423 self.patchset = int(patchset)
1424 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001425 self.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001427 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001428 """Set this branch's issue. If issue isn't given, clears the issue."""
1429 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001431 issue = int(issue)
1432 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001433 self.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001434 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001435 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001436 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001437 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001438 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001439 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 else:
tandrii5d48c322016-08-18 16:19:37 -07001441 # Reset all of these just to be clean.
1442 reset_suffixes = [
1443 'last-upload-hash',
Edward Lemur125d60a2019-09-13 18:25:41 +00001444 self.IssueConfigKey(),
1445 self.PatchsetConfigKey(),
1446 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001447 ] + self._PostUnsetIssueProperties()
1448 for prop in reset_suffixes:
1449 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001450 msg = RunGit(['log', '-1', '--format=%B']).strip()
1451 if msg and git_footers.get_footer_change_id(msg):
1452 print('WARNING: The change patched into this branch has a Change-Id. '
1453 'Removing it.')
1454 RunGit(['commit', '--amend', '-m',
1455 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001456 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001457 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001458 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459
Edward Lemur7f6dec02020-02-06 20:23:58 +00001460 def GetChange(self, upstream_branch):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001461 if not self.GitSanityChecks(upstream_branch):
1462 DieWithError('\nGit sanity check failure')
1463
Edward Lemur7f6dec02020-02-06 20:23:58 +00001464 root = settings.GetRoot()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001465 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001466 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 try:
Edward Lemur7f6dec02020-02-06 20:23:58 +00001468 files = scm.GIT.CaptureStatus(root, upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 except subprocess2.CalledProcessError:
1470 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001471 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001472 'This branch probably doesn\'t exist anymore. To reset the\n'
1473 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001474 ' git branch --set-upstream-to origin/master %s\n'
1475 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001476 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001477
maruel@chromium.org52424302012-08-29 15:14:30 +00001478 issue = self.GetIssue()
1479 patchset = self.GetPatchset()
Edward Lemur7f6dec02020-02-06 20:23:58 +00001480 if issue:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001481 description = self.FetchDescription()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001482 else:
1483 # If the change was never uploaded, use the log messages of all commits
1484 # up to the branch point, as git cl upload will prefill the description
1485 # with these log messages.
Edward Lemur7f6dec02020-02-06 20:23:58 +00001486 description = self.GetLocalDescription(upstream_branch)
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001487
Edward Lemur7f6dec02020-02-06 20:23:58 +00001488 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001489 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490 name,
1491 description,
Edward Lemur7f6dec02020-02-06 20:23:58 +00001492 root,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493 files,
1494 issue,
1495 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001496 author,
1497 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001498
dsansomee2d6fd92016-09-08 00:10:47 -07001499 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001500 assert self.GetIssue(), 'issue is required to update description'
1501
1502 if gerrit_util.HasPendingChangeEdit(
1503 self._GetGerritHost(), self._GerritChangeIdentifier()):
1504 if not force:
1505 confirm_or_exit(
1506 'The description cannot be modified while the issue has a pending '
1507 'unpublished edit. Either publish the edit in the Gerrit web UI '
1508 'or delete it.\n\n', action='delete the unpublished edit')
1509
1510 gerrit_util.DeletePendingChangeEdit(
1511 self._GetGerritHost(), self._GerritChangeIdentifier())
1512 gerrit_util.SetCommitMessage(
1513 self._GetGerritHost(), self._GerritChangeIdentifier(),
1514 description, notify='NONE')
1515
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001516 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001517 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001518
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001519
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001520
Edward Lesmes8e282792018-04-03 18:50:29 -04001521 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1523 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001524 start = time_time()
1525 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001526 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1527 default_presubmit=None, may_prompt=may_prompt,
Edward Lemur125d60a2019-09-13 18:25:41 +00001528 gerrit_obj=self.GetGerritObjForPresubmit(),
Edward Lesmes8e282792018-04-03 18:50:29 -04001529 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001530 metrics.collector.add_repeated('sub_commands', {
1531 'command': 'presubmit',
1532 'execution_time': time_time() - start,
1533 'exit_code': 0 if result.should_continue() else 1,
1534 })
1535 return result
vapierfd77ac72016-06-16 08:33:57 -07001536 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001537 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001538
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001539 def CMDUpload(self, options, git_diff_args, orig_args):
1540 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001541 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001542 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001543 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001544 else:
1545 if self.GetBranch() is None:
1546 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1547
1548 # Default to diffing against common ancestor of upstream branch
1549 base_branch = self.GetCommonAncestorWithUpstream()
1550 git_diff_args = [base_branch, 'HEAD']
1551
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001552 # Fast best-effort checks to abort before running potentially expensive
1553 # hooks if uploading is likely to fail anyway. Passing these checks does
1554 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001555 self.EnsureAuthenticated(force=options.force)
1556 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001557
1558 # Apply watchlists on upload.
Edward Lemur7f6dec02020-02-06 20:23:58 +00001559 change = self.GetChange(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1561 files = [f.LocalPath() for f in change.AffectedFiles()]
1562 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001563 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001564
1565 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001566 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001567 # Set the reviewer list now so that presubmit checks can access it.
1568 change_description = ChangeDescription(change.FullDescriptionText())
1569 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001570 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001571 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001572 change)
1573 change.SetDescriptionText(change_description.description)
1574 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001575 may_prompt=not options.force,
1576 verbose=options.verbose,
1577 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001578 if not hook_results.should_continue():
1579 return 1
1580 if not options.reviewers and hook_results.reviewers:
1581 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001582 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583
Aaron Gable13101a62018-02-09 13:20:41 -08001584 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001585 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001587 _git_set_branch_config_value('last-upload-hash',
1588 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589 # Run post upload hooks, if specified.
1590 if settings.GetRunPostUploadHook():
1591 presubmit_support.DoPostUploadExecuter(
1592 change,
Edward Lemur016a0872020-02-04 22:13:28 +00001593 self.GetGerritObjForPresubmit(),
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 settings.GetRoot(),
1595 options.verbose,
1596 sys.stdout)
1597
1598 # Upload all dependencies if specified.
1599 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001600 print()
1601 print('--dependencies has been specified.')
1602 print('All dependent local branches will be re-uploaded.')
1603 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001604 # Remove the dependencies flag from args so that we do not end up in a
1605 # loop.
1606 orig_args.remove('--dependencies')
1607 ret = upload_branch_deps(self, orig_args)
1608 return ret
1609
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001610 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001611 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001612
1613 Issue must have been already uploaded and known.
1614 """
1615 assert new_state in _CQState.ALL_STATES
1616 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001617 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001618 vote_map = {
1619 _CQState.NONE: 0,
1620 _CQState.DRY_RUN: 1,
1621 _CQState.COMMIT: 2,
1622 }
1623 labels = {'Commit-Queue': vote_map[new_state]}
1624 notify = False if new_state == _CQState.DRY_RUN else None
1625 gerrit_util.SetReview(
1626 self._GetGerritHost(), self._GerritChangeIdentifier(),
1627 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001628 return 0
1629 except KeyboardInterrupt:
1630 raise
1631 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001632 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001633 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001634 ' * Your project has no CQ,\n'
1635 ' * You don\'t have permission to change the CQ state,\n'
1636 ' * There\'s a bug in this code (see stack trace below).\n'
1637 'Consider specifying which bots to trigger manually or asking your '
1638 'project owners for permissions or contacting Chrome Infra at:\n'
1639 'https://www.chromium.org/infra\n\n' %
1640 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001641 # Still raise exception so that stack trace is printed.
1642 raise
1643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 def _GetGerritHost(self):
1645 # Lazy load of configs.
1646 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001647 if self._gerrit_host and '.' not in self._gerrit_host:
1648 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1649 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001650 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001651 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001652 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001653 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001654 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1655 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001656 return self._gerrit_host
1657
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001658 def _GetGitHost(self):
1659 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001660 remote_url = self.GetRemoteUrl()
1661 if not remote_url:
1662 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001663 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001664
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001665 def GetCodereviewServer(self):
1666 if not self._gerrit_server:
1667 # If we're on a branch then get the server potentially associated
1668 # with that branch.
1669 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001670 self._gerrit_server = self._GitGetBranchConfigValue(
1671 self.CodereviewServerConfigKey())
1672 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001673 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001674 if not self._gerrit_server:
1675 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1676 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001677 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001678 parts[0] = parts[0] + '-review'
1679 self._gerrit_host = '.'.join(parts)
1680 self._gerrit_server = 'https://%s' % self._gerrit_host
1681 return self._gerrit_server
1682
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001683 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001684 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001685 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001686 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001687 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001688 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001689 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001690 if project.endswith('.git'):
1691 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001692 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1693 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1694 # gitiles/git-over-https protocol. E.g.,
1695 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1696 # as
1697 # https://chromium.googlesource.com/v8/v8
1698 if project.startswith('a/'):
1699 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001700 return project
1701
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001702 def _GerritChangeIdentifier(self):
1703 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1704
1705 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001706 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001707 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001708 project = self._GetGerritProject()
1709 if project:
1710 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1711 # Fall back on still unique, but less efficient change number.
1712 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001713
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001714 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001715 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001716 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717
tandrii5d48c322016-08-18 16:19:37 -07001718 @classmethod
1719 def PatchsetConfigKey(cls):
1720 return 'gerritpatchset'
1721
1722 @classmethod
1723 def CodereviewServerConfigKey(cls):
1724 return 'gerritserver'
1725
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001726 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001727 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001728 if settings.GetGerritSkipEnsureAuthenticated():
1729 # For projects with unusual authentication schemes.
1730 # See http://crbug.com/603378.
1731 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001732
1733 # Check presence of cookies only if using cookies-based auth method.
1734 cookie_auth = gerrit_util.Authenticator.get()
1735 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001736 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001737
Florian Mayerae510e82020-01-30 21:04:48 +00001738 remote_url = self.GetRemoteUrl()
1739 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001740 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001741 return
1742 if urllib.parse.urlparse(remote_url).scheme != 'https':
Josip906bfde2020-01-31 22:38:49 +00001743 logging.warning('Ignoring branch %(branch)s with non-https remote '
1744 '%(remote)s', {
1745 'branch': self.branch,
1746 'remote': self.GetRemoteUrl()
1747 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001748 return
1749
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001750 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001751 self.GetCodereviewServer()
1752 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001753 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001754
1755 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1756 git_auth = cookie_auth.get_auth_header(git_host)
1757 if gerrit_auth and git_auth:
1758 if gerrit_auth == git_auth:
1759 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001760 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001761 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001762 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001763 ' %s\n'
1764 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001765 ' Consider running the following command:\n'
1766 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001767 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001768 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001769 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001770 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001771 cookie_auth.get_new_password_message(git_host)))
1772 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001773 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001774 return
1775 else:
1776 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001777 ([] if gerrit_auth else [self._gerrit_host]) +
1778 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001779 DieWithError('Credentials for the following hosts are required:\n'
1780 ' %s\n'
1781 'These are read from %s (or legacy %s)\n'
1782 '%s' % (
1783 '\n '.join(missing),
1784 cookie_auth.get_gitcookies_path(),
1785 cookie_auth.get_netrc_path(),
1786 cookie_auth.get_new_password_message(git_host)))
1787
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001788 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001789 if not self.GetIssue():
1790 return
1791
1792 # Warm change details cache now to avoid RPCs later, reducing latency for
1793 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001794 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001795 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001796
1797 status = self._GetChangeDetail()['status']
1798 if status in ('MERGED', 'ABANDONED'):
1799 DieWithError('Change %s has been %s, new uploads are not allowed' %
1800 (self.GetIssueURL(),
1801 'submitted' if status == 'MERGED' else 'abandoned'))
1802
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001803 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1804 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1805 # Apparently this check is not very important? Otherwise get_auth_email
1806 # could have been added to other implementations of Authenticator.
1807 cookies_auth = gerrit_util.Authenticator.get()
1808 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001809 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001810
1811 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001812 if self.GetIssueOwner() == cookies_user:
1813 return
1814 logging.debug('change %s owner is %s, cookies user is %s',
1815 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001816 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001817 # so ask what Gerrit thinks of this user.
1818 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1819 if details['email'] == self.GetIssueOwner():
1820 return
1821 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001822 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001823 'as %s.\n'
1824 'Uploading may fail due to lack of permissions.' %
1825 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1826 confirm_or_exit(action='upload')
1827
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001828 def _PostUnsetIssueProperties(self):
1829 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001830 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001831
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001832 def GetGerritObjForPresubmit(self):
1833 return presubmit_support.GerritAccessor(self._GetGerritHost())
1834
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001835 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001836 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001837 or CQ status, assuming adherence to a common workflow.
1838
1839 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001840 * 'error' - error from review tool (including deleted issues)
1841 * 'unsent' - no reviewers added
1842 * 'waiting' - waiting for review
1843 * 'reply' - waiting for uploader to reply to review
1844 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001845 * 'dry-run' - dry-running in the CQ
1846 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001847 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001848 """
1849 if not self.GetIssue():
1850 return None
1851
1852 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001853 data = self._GetChangeDetail([
1854 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001855 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001856 return 'error'
1857
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001858 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001859 return 'closed'
1860
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001861 cq_label = data['labels'].get('Commit-Queue', {})
1862 max_cq_vote = 0
1863 for vote in cq_label.get('all', []):
1864 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1865 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001866 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001867 if max_cq_vote == 1:
1868 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001869
Aaron Gable9ab38c62017-04-06 14:36:33 -07001870 if data['labels'].get('Code-Review', {}).get('approved'):
1871 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001872
1873 if not data.get('reviewers', {}).get('REVIEWER', []):
1874 return 'unsent'
1875
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001876 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001877 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Aaron Gable9ab38c62017-04-06 14:36:33 -07001878 last_message_author = messages.pop().get('author', {})
1879 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001880 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
1881 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07001882 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001883 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07001884 if last_message_author.get('_account_id') == owner:
1885 # Most recent message was by owner.
1886 return 'waiting'
1887 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001888 # Some reply from non-owner.
1889 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001890
1891 # Somehow there are no messages even though there are reviewers.
1892 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001893
1894 def GetMostRecentPatchset(self):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001895 if not self.GetIssue():
1896 return None
1897
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001898 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001899 patchset = data['revisions'][data['current_revision']]['_number']
1900 self.SetPatchset(patchset)
1901 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001902
Aaron Gable636b13f2017-07-14 10:42:48 -07001903 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001904 gerrit_util.SetReview(
1905 self._GetGerritHost(), self._GerritChangeIdentifier(),
1906 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001907
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001908 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001909 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001910 # CURRENT_REVISION is included to get the latest patchset so that
1911 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001912 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001913 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1914 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001915 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001916 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001917 robot_file_comments = gerrit_util.GetChangeRobotComments(
1918 self._GetGerritHost(), self._GerritChangeIdentifier())
1919
1920 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001921 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001922 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001923 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001924 line_comments = file_comments.setdefault(path, [])
1925 line_comments.extend(
1926 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001927
1928 # Build dictionary of file comments for easy access and sorting later.
1929 # {author+date: {path: {patchset: {line: url+message}}}}
1930 comments = collections.defaultdict(
1931 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001932 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001933 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001934 tag = comment.get('tag', '')
1935 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001936 continue
1937 key = (comment['author']['email'], comment['updated'])
1938 if comment.get('side', 'REVISION') == 'PARENT':
1939 patchset = 'Base'
1940 else:
1941 patchset = 'PS%d' % comment['patch_set']
1942 line = comment.get('line', 0)
1943 url = ('https://%s/c/%s/%s/%s#%s%s' %
1944 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1945 'b' if comment.get('side') == 'PARENT' else '',
1946 str(line) if line else ''))
1947 comments[key][path][patchset][line] = (url, comment['message'])
1948
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001949 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001950 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001951 summary = self._BuildCommentSummary(msg, comments, readable)
1952 if summary:
1953 summaries.append(summary)
1954 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001955
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001956 @staticmethod
1957 def _BuildCommentSummary(msg, comments, readable):
1958 key = (msg['author']['email'], msg['date'])
1959 # Don't bother showing autogenerated messages that don't have associated
1960 # file or line comments. this will filter out most autogenerated
1961 # messages, but will keep robot comments like those from Tricium.
1962 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1963 if is_autogenerated and not comments.get(key):
1964 return None
1965 message = msg['message']
1966 # Gerrit spits out nanoseconds.
1967 assert len(msg['date'].split('.')[-1]) == 9
1968 date = datetime.datetime.strptime(msg['date'][:-3],
1969 '%Y-%m-%d %H:%M:%S.%f')
1970 if key in comments:
1971 message += '\n'
1972 for path, patchsets in sorted(comments.get(key, {}).items()):
1973 if readable:
1974 message += '\n%s' % path
1975 for patchset, lines in sorted(patchsets.items()):
1976 for line, (url, content) in sorted(lines.items()):
1977 if line:
1978 line_str = 'Line %d' % line
1979 path_str = '%s:%d:' % (path, line)
1980 else:
1981 line_str = 'File comment'
1982 path_str = '%s:0:' % path
1983 if readable:
1984 message += '\n %s, %s: %s' % (patchset, line_str, url)
1985 message += '\n %s\n' % content
1986 else:
1987 message += '\n%s ' % path_str
1988 message += '\n%s\n' % content
1989
1990 return _CommentSummary(
1991 date=date,
1992 message=message,
1993 sender=msg['author']['email'],
1994 autogenerated=is_autogenerated,
1995 # These could be inferred from the text messages and correlated with
1996 # Code-Review label maximum, however this is not reliable.
1997 # Leaving as is until the need arises.
1998 approval=False,
1999 disapproval=False,
2000 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002001
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002002 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002003 gerrit_util.AbandonChange(
2004 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002005
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002006 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002007 gerrit_util.SubmitChange(
2008 self._GetGerritHost(), self._GerritChangeIdentifier(),
2009 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002010
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002011 def _GetChangeDetail(self, options=None, no_cache=False):
2012 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002013
2014 If fresh data is needed, set no_cache=True which will clear cache and
2015 thus new data will be fetched from Gerrit.
2016 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002017 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002018 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002019
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002020 # Optimization to avoid multiple RPCs:
2021 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2022 'CURRENT_COMMIT' not in options):
2023 options.append('CURRENT_COMMIT')
2024
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002025 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002026 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002027 options = [o.upper() for o in options]
2028
2029 # Check in cache first unless no_cache is True.
2030 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002031 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002032 else:
2033 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002034 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002035 # Assumption: data fetched before with extra options is suitable
2036 # for return for a smaller set of options.
2037 # For example, if we cached data for
2038 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2039 # and request is for options=[CURRENT_REVISION],
2040 # THEN we can return prior cached data.
2041 if options_set.issubset(cached_options_set):
2042 return data
2043
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002044 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002045 data = gerrit_util.GetChangeDetail(
2046 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002047 except gerrit_util.GerritError as e:
2048 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002049 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002050 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002051
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002052 self._detail_cache.setdefault(cache_key, []).append(
2053 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002054 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002055
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002056 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002057 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002058 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002059 data = gerrit_util.GetChangeCommit(
2060 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002061 except gerrit_util.GerritError as e:
2062 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002063 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002064 raise
agable32978d92016-11-01 12:55:02 -07002065 return data
2066
Karen Qian40c19422019-03-13 21:28:29 +00002067 def _IsCqConfigured(self):
2068 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00002069 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00002070
Olivier Robin75ee7252018-04-13 10:02:56 +02002071 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002072 if git_common.is_dirty_git_tree('land'):
2073 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002074
tandriid60367b2016-06-22 05:25:12 -07002075 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002076 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002077 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002078 'which can test and land changes for you. '
2079 'Are you sure you wish to bypass it?\n',
2080 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002081 differs = True
tandriic4344b52016-08-29 06:04:54 -07002082 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002083 # Note: git diff outputs nothing if there is no diff.
2084 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002085 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002086 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002087 if detail['current_revision'] == last_upload:
2088 differs = False
2089 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002090 print('WARNING: Local branch contents differ from latest uploaded '
2091 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002092 if differs:
2093 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002094 confirm_or_exit(
2095 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2096 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002097 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002098 elif not bypass_hooks:
2099 hook_results = self.RunHook(
2100 committing=True,
2101 may_prompt=not force,
2102 verbose=verbose,
Edward Lemur7f6dec02020-02-06 20:23:58 +00002103 change=self.GetChange(self.GetCommonAncestorWithUpstream()),
Olivier Robin75ee7252018-04-13 10:02:56 +02002104 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002105 if not hook_results.should_continue():
2106 return 1
2107
2108 self.SubmitIssue(wait_for_merge=True)
2109 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002110 links = self._GetChangeCommit().get('web_links', [])
2111 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002112 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002113 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002114 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002115 return 0
2116
Edward Lemurf38bc172019-09-03 21:02:13 +00002117 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 assert parsed_issue_arg.valid
2119
Edward Lemur125d60a2019-09-13 18:25:41 +00002120 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002121
2122 if parsed_issue_arg.hostname:
2123 self._gerrit_host = parsed_issue_arg.hostname
2124 self._gerrit_server = 'https://%s' % self._gerrit_host
2125
tandriic2405f52016-10-10 08:13:15 -07002126 try:
2127 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002128 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002129 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002130
2131 if not parsed_issue_arg.patchset:
2132 # Use current revision by default.
2133 revision_info = detail['revisions'][detail['current_revision']]
2134 patchset = int(revision_info['_number'])
2135 else:
2136 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002137 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002138 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2139 break
2140 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002141 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002142 (parsed_issue_arg.patchset, self.GetIssue()))
2143
Edward Lemur125d60a2019-09-13 18:25:41 +00002144 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002145 if remote_url.endswith('.git'):
2146 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002147 remote_url = remote_url.rstrip('/')
2148
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002149 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002150 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002151
2152 if remote_url != fetch_info['url']:
2153 DieWithError('Trying to patch a change from %s but this repo appears '
2154 'to be %s.' % (fetch_info['url'], remote_url))
2155
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002157
Aaron Gable62619a32017-06-16 08:22:09 -07002158 if force:
2159 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2160 print('Checked out commit for change %i patchset %i locally' %
2161 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002162 elif nocommit:
2163 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2164 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002165 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002166 RunGit(['cherry-pick', 'FETCH_HEAD'])
2167 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002168 (parsed_issue_arg.issue, patchset))
2169 print('Note: this created a local commit which does not have '
2170 'the same hash as the one uploaded for review. This will make '
2171 'uploading changes based on top of this branch difficult.\n'
2172 'If you want to do that, use "git cl patch --force" instead.')
2173
Stefan Zagerd08043c2017-10-12 12:07:02 -07002174 if self.GetBranch():
2175 self.SetIssue(parsed_issue_arg.issue)
2176 self.SetPatchset(patchset)
2177 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2178 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2179 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2180 else:
2181 print('WARNING: You are in detached HEAD state.\n'
2182 'The patch has been applied to your checkout, but you will not be '
2183 'able to upload a new patch set to the gerrit issue.\n'
2184 'Try using the \'-b\' option if you would like to work on a '
2185 'branch and/or upload a new patch set.')
2186
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002187 return 0
2188
tandrii16e0b4e2016-06-07 10:34:28 -07002189 def _GerritCommitMsgHookCheck(self, offer_removal):
2190 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2191 if not os.path.exists(hook):
2192 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002193 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2194 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002195 data = gclient_utils.FileRead(hook)
2196 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2197 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002198 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002199 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002200 'and may interfere with it in subtle ways.\n'
2201 'We recommend you remove the commit-msg hook.')
2202 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002203 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002204 gclient_utils.rm_file_or_tree(hook)
2205 print('Gerrit commit-msg hook removed.')
2206 else:
2207 print('OK, will keep Gerrit commit-msg hook in place.')
2208
Edward Lemur1b52d872019-05-09 21:12:12 +00002209 def _CleanUpOldTraces(self):
2210 """Keep only the last |MAX_TRACES| traces."""
2211 try:
2212 traces = sorted([
2213 os.path.join(TRACES_DIR, f)
2214 for f in os.listdir(TRACES_DIR)
2215 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2216 and not f.startswith('tmp'))
2217 ])
2218 traces_to_delete = traces[:-MAX_TRACES]
2219 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002220 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002221 except OSError:
2222 print('WARNING: Failed to remove old git traces from\n'
2223 ' %s'
2224 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002225
Edward Lemur5737f022019-05-17 01:24:00 +00002226 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002227 """Zip and write the git push traces stored in traces_dir."""
2228 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002229 traces_zip = trace_name + '-traces'
2230 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002231 # Create a temporary dir to store git config and gitcookies in. It will be
2232 # compressed and stored next to the traces.
2233 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002234 git_info_zip = trace_name + '-git-info'
2235
Edward Lemur5737f022019-05-17 01:24:00 +00002236 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002237 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002238 git_push_metadata['now'] = git_push_metadata['now'].decode(
2239 sys.stdin.encoding)
2240
Edward Lemur1b52d872019-05-09 21:12:12 +00002241 git_push_metadata['trace_name'] = trace_name
2242 gclient_utils.FileWrite(
2243 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2244
2245 # Keep only the first 6 characters of the git hashes on the packet
2246 # trace. This greatly decreases size after compression.
2247 packet_traces = os.path.join(traces_dir, 'trace-packet')
2248 if os.path.isfile(packet_traces):
2249 contents = gclient_utils.FileRead(packet_traces)
2250 gclient_utils.FileWrite(
2251 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2252 shutil.make_archive(traces_zip, 'zip', traces_dir)
2253
2254 # Collect and compress the git config and gitcookies.
2255 git_config = RunGit(['config', '-l'])
2256 gclient_utils.FileWrite(
2257 os.path.join(git_info_dir, 'git-config'),
2258 git_config)
2259
2260 cookie_auth = gerrit_util.Authenticator.get()
2261 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2262 gitcookies_path = cookie_auth.get_gitcookies_path()
2263 if os.path.isfile(gitcookies_path):
2264 gitcookies = gclient_utils.FileRead(gitcookies_path)
2265 gclient_utils.FileWrite(
2266 os.path.join(git_info_dir, 'gitcookies'),
2267 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2268 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2269
Edward Lemur1b52d872019-05-09 21:12:12 +00002270 gclient_utils.rmtree(git_info_dir)
2271
2272 def _RunGitPushWithTraces(
2273 self, change_desc, refspec, refspec_opts, git_push_metadata):
2274 """Run git push and collect the traces resulting from the execution."""
2275 # Create a temporary directory to store traces in. Traces will be compressed
2276 # and stored in a 'traces' dir inside depot_tools.
2277 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002278 trace_name = os.path.join(
2279 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002280
2281 env = os.environ.copy()
2282 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2283 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002284 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002285 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2286 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2287 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2288
2289 try:
2290 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002291 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002292 before_push = time_time()
2293 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002294 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002295 env=env,
2296 print_stdout=True,
2297 # Flush after every line: useful for seeing progress when running as
2298 # recipe.
2299 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002300 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002301 except subprocess2.CalledProcessError as e:
2302 push_returncode = e.returncode
2303 DieWithError('Failed to create a change. Please examine output above '
2304 'for the reason of the failure.\n'
2305 'Hint: run command below to diagnose common Git/Gerrit '
2306 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002307 ' git cl creds-check\n'
2308 '\n'
2309 'If git-cl is not working correctly, file a bug under the '
2310 'Infra>SDK component including the files below.\n'
2311 'Review the files before upload, since they might contain '
2312 'sensitive information.\n'
2313 'Set the Restrict-View-Google label so that they are not '
2314 'publicly accessible.\n'
2315 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002316 change_desc)
2317 finally:
2318 execution_time = time_time() - before_push
2319 metrics.collector.add_repeated('sub_commands', {
2320 'command': 'git push',
2321 'execution_time': execution_time,
2322 'exit_code': push_returncode,
2323 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2324 })
2325
Edward Lemur1b52d872019-05-09 21:12:12 +00002326 git_push_metadata['execution_time'] = execution_time
2327 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002328 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002329
Edward Lemur1b52d872019-05-09 21:12:12 +00002330 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002331 gclient_utils.rmtree(traces_dir)
2332
2333 return push_stdout
2334
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002335 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002336 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002337 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002338 # Load default for user, repo, squash=true, in this order.
2339 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002340
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002341 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002342 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002343 # This may be None; default fallback value is determined in logic below.
2344 title = options.title
2345
Dominic Battre7d1c4842017-10-27 09:17:28 +02002346 # Extract bug number from branch name.
2347 bug = options.bug
Dan Beamd8b04ca2019-10-10 21:23:26 +00002348 fixed = options.fixed
2349 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
2350 self.GetBranch())
2351 if not bug and not fixed and match:
2352 if match.group('type') == 'bug':
2353 bug = match.group('bugnum')
2354 else:
2355 fixed = match.group('bugnum')
Dominic Battre7d1c4842017-10-27 09:17:28 +02002356
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002357 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002358 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002359 if self.GetIssue():
2360 # Try to get the message from a previous upload.
Edward Lemur6c6827c2020-02-06 21:15:18 +00002361 message = self.FetchDescription()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002362 if not message:
2363 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002364 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002365 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002366 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002367 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002368 # When uploading a subsequent patchset, -m|--message is taken
2369 # as the patchset title if --title was not provided.
2370 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002371 else:
2372 default_title = RunGit(
2373 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002374 if options.force:
2375 title = default_title
2376 else:
2377 title = ask_for_data(
2378 'Title for patchset [%s]: ' % default_title) or default_title
Josipe827b0f2020-01-30 00:07:20 +00002379
2380 # User requested to change description
2381 if options.edit_description:
2382 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
2383 change_desc.prompt()
2384 message = change_desc.description
2385
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002386 change_id = self._GetChangeDetail()['change_id']
2387 while True:
2388 footer_change_ids = git_footers.get_footer_change_id(message)
2389 if footer_change_ids == [change_id]:
2390 break
2391 if not footer_change_ids:
2392 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002393 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002394 continue
2395 # There is already a valid footer but with different or several ids.
2396 # Doing this automatically is non-trivial as we don't want to lose
2397 # existing other footers, yet we want to append just 1 desired
2398 # Change-Id. Thus, just create a new footer, but let user verify the
2399 # new description.
2400 message = '%s\n\nChange-Id: %s' % (message, change_id)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002401 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002402 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002403 print(
2404 'WARNING: change %s has Change-Id footer(s):\n'
2405 ' %s\n'
2406 'but change has Change-Id %s, according to Gerrit.\n'
2407 'Please, check the proposed correction to the description, '
2408 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2409 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2410 change_id))
2411 confirm_or_exit(action='edit')
2412 change_desc.prompt()
2413
2414 message = change_desc.description
2415 if not message:
2416 DieWithError("Description is empty. Aborting...")
2417
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002418 # Continue the while loop.
2419 # Sanity check of this code - we should end up with proper message
2420 # footer.
2421 assert [change_id] == git_footers.get_footer_change_id(message)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002422 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
Aaron Gableb56ad332017-01-06 15:24:31 -08002423 else: # if not self.GetIssue()
2424 if options.message:
2425 message = options.message
2426 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002427 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002428 if options.title:
2429 message = options.title + '\n\n' + message
Dan Beamd8b04ca2019-10-10 21:23:26 +00002430 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002431 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002432 change_desc.prompt()
2433
Aaron Gableb56ad332017-01-06 15:24:31 -08002434 # On first upload, patchset title is always this string, while
2435 # --title flag gets converted to first line of message.
2436 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002437 if not change_desc.description:
2438 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002439 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002440 if len(change_ids) > 1:
2441 DieWithError('too many Change-Id footers, at most 1 allowed.')
2442 if not change_ids:
2443 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002444 change_desc.set_description(git_footers.add_footer_change_id(
2445 change_desc.description,
2446 GenerateGerritChangeId(change_desc.description)))
2447 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002448 assert len(change_ids) == 1
2449 change_id = change_ids[0]
2450
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002451 if options.reviewers or options.tbrs or options.add_owners_to:
2452 change_desc.update_reviewers(options.reviewers, options.tbrs,
2453 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002454 if options.preserve_tryjobs:
2455 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002456
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002457 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002458 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2459 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002460 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lesmesf6a22322019-11-04 22:14:39 +00002461 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
Edward Lemur79d4f992019-11-11 23:49:02 +00002462 desc_tempfile.write(change_desc.description.encode('utf-8', 'replace'))
Aaron Gable9a03ae02017-11-03 11:31:07 -07002463 desc_tempfile.close()
2464 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2465 '-F', desc_tempfile.name]).strip()
2466 os.remove(desc_tempfile.name)
Anthony Polito8b955342019-09-24 19:01:36 +00002467 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002468 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002469 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 if not change_desc.description:
2471 DieWithError("Description is empty. Aborting...")
2472
2473 if not git_footers.get_footer_change_id(change_desc.description):
2474 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002475 change_desc.set_description(
2476 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002477 if options.reviewers or options.tbrs or options.add_owners_to:
2478 change_desc.update_reviewers(options.reviewers, options.tbrs,
2479 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002480 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002481 # For no-squash mode, we assume the remote called "origin" is the one we
2482 # want. It is not worthwhile to support different workflows for
2483 # no-squash mode.
2484 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2486
2487 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002488 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002489 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2490 ref_to_push)]).splitlines()
2491 if len(commits) > 1:
2492 print('WARNING: This will upload %d commits. Run the following command '
2493 'to see which commits will be uploaded: ' % len(commits))
2494 print('git log %s..%s' % (parent, ref_to_push))
2495 print('You can also use `git squash-branch` to squash these into a '
2496 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002497 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002498
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002499 if options.reviewers or options.tbrs or options.add_owners_to:
2500 change_desc.update_reviewers(options.reviewers, options.tbrs,
2501 options.add_owners_to, change)
2502
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002503 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002504 cc = []
2505 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2506 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2507 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002508 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002509 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002510 if options.cc:
2511 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002512 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002513 if change_desc.get_cced():
2514 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002515 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2516 valid_accounts = set(reviewers + cc)
2517 # TODO(crbug/877717): relax this for all hosts.
2518 else:
2519 valid_accounts = gerrit_util.ValidAccounts(
2520 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002521 logging.info('accounts %s are recognized, %s invalid',
2522 sorted(valid_accounts),
2523 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002524
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002525 # Extra options that can be specified at push time. Doc:
2526 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002527 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002528
Aaron Gable844cf292017-06-28 11:32:59 -07002529 # By default, new changes are started in WIP mode, and subsequent patchsets
2530 # don't send email. At any time, passing --send-mail will mark the change
2531 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002532 if options.send_mail:
2533 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002534 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002535 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002536 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002537 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002538 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002539
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002540 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002541 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002542
Aaron Gable9b713dd2016-12-14 16:04:21 -08002543 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002544 # Punctuation and whitespace in |title| must be percent-encoded.
2545 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002546
agablec6787972016-09-09 16:13:34 -07002547 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002548 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002549
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002550 for r in sorted(reviewers):
2551 if r in valid_accounts:
2552 refspec_opts.append('r=%s' % r)
2553 reviewers.remove(r)
2554 else:
2555 # TODO(tandrii): this should probably be a hard failure.
2556 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2557 % r)
2558 for c in sorted(cc):
2559 # refspec option will be rejected if cc doesn't correspond to an
2560 # account, even though REST call to add such arbitrary cc may succeed.
2561 if c in valid_accounts:
2562 refspec_opts.append('cc=%s' % c)
2563 cc.remove(c)
2564
rmistry9eadede2016-09-19 11:22:43 -07002565 if options.topic:
2566 # Documentation on Gerrit topics is here:
2567 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002568 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002569
Edward Lemur687ca902018-12-05 02:30:30 +00002570 if options.enable_auto_submit:
2571 refspec_opts.append('l=Auto-Submit+1')
2572 if options.use_commit_queue:
2573 refspec_opts.append('l=Commit-Queue+2')
2574 elif options.cq_dry_run:
2575 refspec_opts.append('l=Commit-Queue+1')
2576
2577 if change_desc.get_reviewers(tbr_only=True):
2578 score = gerrit_util.GetCodeReviewTbrScore(
2579 self._GetGerritHost(),
2580 self._GetGerritProject())
2581 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002582
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002583 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002584 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002585 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002586 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002587 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2588
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002589 refspec_suffix = ''
2590 if refspec_opts:
2591 refspec_suffix = '%' + ','.join(refspec_opts)
2592 assert ' ' not in refspec_suffix, (
2593 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2594 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2595
Edward Lemur1b52d872019-05-09 21:12:12 +00002596 git_push_metadata = {
2597 'gerrit_host': self._GetGerritHost(),
2598 'title': title or '<untitled>',
2599 'change_id': change_id,
2600 'description': change_desc.description,
2601 }
2602 push_stdout = self._RunGitPushWithTraces(
2603 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002604
2605 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002606 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002607 change_numbers = [m.group(1)
2608 for m in map(regex.match, push_stdout.splitlines())
2609 if m]
2610 if len(change_numbers) != 1:
2611 DieWithError(
2612 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002613 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002614 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002615 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002616
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002617 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002618 # GetIssue() is not set in case of non-squash uploads according to tests.
2619 # TODO(agable): non-squash uploads in git cl should be removed.
2620 gerrit_util.AddReviewers(
2621 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002622 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002623 reviewers, cc,
2624 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002625
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002626 return 0
2627
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002628 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2629 change_desc):
2630 """Computes parent of the generated commit to be uploaded to Gerrit.
2631
2632 Returns revision or a ref name.
2633 """
2634 if custom_cl_base:
2635 # Try to avoid creating additional unintended CLs when uploading, unless
2636 # user wants to take this risk.
2637 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2638 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2639 local_ref_of_target_remote])
2640 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002641 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002642 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2643 'If you proceed with upload, more than 1 CL may be created by '
2644 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2645 'If you are certain that specified base `%s` has already been '
2646 'uploaded to Gerrit as another CL, you may proceed.\n' %
2647 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2648 if not force:
2649 confirm_or_exit(
2650 'Do you take responsibility for cleaning up potential mess '
2651 'resulting from proceeding with upload?',
2652 action='upload')
2653 return custom_cl_base
2654
Aaron Gablef97e33d2017-03-30 15:44:27 -07002655 if remote != '.':
2656 return self.GetCommonAncestorWithUpstream()
2657
2658 # If our upstream branch is local, we base our squashed commit on its
2659 # squashed version.
2660 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2661
Aaron Gablef97e33d2017-03-30 15:44:27 -07002662 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002663 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002664
2665 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002666 # TODO(tandrii): consider checking parent change in Gerrit and using its
2667 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2668 # the tree hash of the parent branch. The upside is less likely bogus
2669 # requests to reupload parent change just because it's uploadhash is
2670 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002671 parent = RunGit(['config',
2672 'branch.%s.gerritsquashhash' % upstream_branch_name],
2673 error_ok=True).strip()
2674 # Verify that the upstream branch has been uploaded too, otherwise
2675 # Gerrit will create additional CLs when uploading.
2676 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2677 RunGitSilent(['rev-parse', parent + ':'])):
2678 DieWithError(
2679 '\nUpload upstream branch %s first.\n'
2680 'It is likely that this branch has been rebased since its last '
2681 'upload, so you just need to upload it again.\n'
2682 '(If you uploaded it with --no-squash, then branch dependencies '
2683 'are not supported, and you should reupload with --squash.)'
2684 % upstream_branch_name,
2685 change_desc)
2686 return parent
2687
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002688 def _AddChangeIdToCommitMessage(self, options, args):
2689 """Re-commits using the current message, assumes the commit hook is in
2690 place.
2691 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002692 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002693 git_command = ['commit', '--amend', '-m', log_desc]
2694 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002695 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002696 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002697 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002698 return new_log_desc
2699 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002700 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002701
tandriie113dfd2016-10-11 10:20:12 -07002702 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002703 try:
2704 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002705 except GerritChangeNotExists:
2706 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002707
2708 if data['status'] in ('ABANDONED', 'MERGED'):
2709 return 'CL %s is closed' % self.GetIssue()
2710
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002711 def GetGerritChange(self, patchset=None):
2712 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002713 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002714 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002715 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002716 data = self._GetChangeDetail(['ALL_REVISIONS'])
2717
2718 assert host and issue and patchset, 'CL must be uploaded first'
2719
2720 has_patchset = any(
2721 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002722 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002723 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002724 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002725 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002726
tandrii8c5a3532016-11-04 07:52:02 -07002727 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002728 'host': host,
2729 'change': issue,
2730 'project': data['project'],
2731 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002732 }
tandriie113dfd2016-10-11 10:20:12 -07002733
tandriide281ae2016-10-12 06:02:30 -07002734 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002735 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002736
Edward Lemur707d70b2018-02-07 00:50:14 +01002737 def GetReviewers(self):
2738 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002739 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002740
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002741
tandriif9aefb72016-07-01 09:06:51 -07002742def _get_bug_line_values(default_project, bugs):
2743 """Given default_project and comma separated list of bugs, yields bug line
2744 values.
2745
2746 Each bug can be either:
2747 * a number, which is combined with default_project
2748 * string, which is left as is.
2749
2750 This function may produce more than one line, because bugdroid expects one
2751 project per line.
2752
2753 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2754 ['v8:123', 'chromium:789']
2755 """
2756 default_bugs = []
2757 others = []
2758 for bug in bugs.split(','):
2759 bug = bug.strip()
2760 if bug:
2761 try:
2762 default_bugs.append(int(bug))
2763 except ValueError:
2764 others.append(bug)
2765
2766 if default_bugs:
2767 default_bugs = ','.join(map(str, default_bugs))
2768 if default_project:
2769 yield '%s:%s' % (default_project, default_bugs)
2770 else:
2771 yield default_bugs
2772 for other in sorted(others):
2773 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2774 yield other
2775
2776
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002777class ChangeDescription(object):
2778 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002779 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002780 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002781 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002782 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002783 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002784 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2785 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002786 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002787 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002788
Dan Beamd8b04ca2019-10-10 21:23:26 +00002789 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002790 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002791 if bug:
2792 regexp = re.compile(self.BUG_LINE)
2793 prefix = settings.GetBugPrefix()
2794 if not any((regexp.match(line) for line in self._description_lines)):
2795 values = list(_get_bug_line_values(prefix, bug))
2796 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002797 if fixed:
2798 regexp = re.compile(self.FIXED_LINE)
2799 prefix = settings.GetBugPrefix()
2800 if not any((regexp.match(line) for line in self._description_lines)):
2801 values = list(_get_bug_line_values(prefix, fixed))
2802 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002803
agable@chromium.org42c20792013-09-12 17:34:49 +00002804 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002805 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002806 return '\n'.join(self._description_lines)
2807
2808 def set_description(self, desc):
2809 if isinstance(desc, basestring):
2810 lines = desc.splitlines()
2811 else:
2812 lines = [line.rstrip() for line in desc]
2813 while lines and not lines[0]:
2814 lines.pop(0)
2815 while lines and not lines[-1]:
2816 lines.pop(-1)
2817 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002818
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002819 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2820 """Rewrites the R=/TBR= line(s) as a single line each.
2821
2822 Args:
2823 reviewers (list(str)) - list of additional emails to use for reviewers.
2824 tbrs (list(str)) - list of additional emails to use for TBRs.
2825 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2826 the change that are missing OWNER coverage. If this is not None, you
2827 must also pass a value for `change`.
2828 change (Change) - The Change that should be used for OWNERS lookups.
2829 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002830 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002831 assert isinstance(tbrs, list), tbrs
2832
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002833 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002834 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002835
2836 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002837 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002838
2839 reviewers = set(reviewers)
2840 tbrs = set(tbrs)
2841 LOOKUP = {
2842 'TBR': tbrs,
2843 'R': reviewers,
2844 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002845
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002846 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002847 regexp = re.compile(self.R_LINE)
2848 matches = [regexp.match(line) for line in self._description_lines]
2849 new_desc = [l for i, l in enumerate(self._description_lines)
2850 if not matches[i]]
2851 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002852
agable@chromium.org42c20792013-09-12 17:34:49 +00002853 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002854
2855 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002856 for match in matches:
2857 if not match:
2858 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002859 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2860
2861 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002862 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002863 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02002864 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002865 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002866 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002867 LOOKUP[add_owners_to].update(
2868 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002869
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002870 # If any folks ended up in both groups, remove them from tbrs.
2871 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002872
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002873 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2874 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002875
2876 # Put the new lines in the description where the old first R= line was.
2877 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2878 if 0 <= line_loc < len(self._description_lines):
2879 if new_tbr_line:
2880 self._description_lines.insert(line_loc, new_tbr_line)
2881 if new_r_line:
2882 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002883 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002884 if new_r_line:
2885 self.append_footer(new_r_line)
2886 if new_tbr_line:
2887 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002888
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002889 def set_preserve_tryjobs(self):
2890 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2891 footers = git_footers.parse_footers(self.description)
2892 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2893 if v.lower() == 'true':
2894 return
2895 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2896
Anthony Polito8b955342019-09-24 19:01:36 +00002897 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002898 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002899 self.set_description([
2900 '# Enter a description of the change.',
2901 '# This will be displayed on the codereview site.',
2902 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002903 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002904 '--------------------',
2905 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002906 bug_regexp = re.compile(self.BUG_LINE)
2907 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002908 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002909 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2910 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002911 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002912
agable@chromium.org42c20792013-09-12 17:34:49 +00002913 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002914 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002915 if not content:
2916 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002917 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002918
Bruce Dawson2377b012018-01-11 16:46:49 -08002919 # Strip off comments and default inserted "Bug:" line.
2920 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002921 (line.startswith('#') or
2922 line.rstrip() == "Bug:" or
2923 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002924 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002925 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002926 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002927
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002928 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002929 """Adds a footer line to the description.
2930
2931 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2932 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2933 that Gerrit footers are always at the end.
2934 """
2935 parsed_footer_line = git_footers.parse_footer(line)
2936 if parsed_footer_line:
2937 # Line is a gerrit footer in the form: Footer-Key: any value.
2938 # Thus, must be appended observing Gerrit footer rules.
2939 self.set_description(
2940 git_footers.add_footer(self.description,
2941 key=parsed_footer_line[0],
2942 value=parsed_footer_line[1]))
2943 return
2944
2945 if not self._description_lines:
2946 self._description_lines.append(line)
2947 return
2948
2949 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2950 if gerrit_footers:
2951 # git_footers.split_footers ensures that there is an empty line before
2952 # actual (gerrit) footers, if any. We have to keep it that way.
2953 assert top_lines and top_lines[-1] == ''
2954 top_lines, separator = top_lines[:-1], top_lines[-1:]
2955 else:
2956 separator = [] # No need for separator if there are no gerrit_footers.
2957
2958 prev_line = top_lines[-1] if top_lines else ''
2959 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2960 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2961 top_lines.append('')
2962 top_lines.append(line)
2963 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002964
tandrii99a72f22016-08-17 14:33:24 -07002965 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002966 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002967 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002968 reviewers = [match.group(2).strip()
2969 for match in matches
2970 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002971 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002972
bradnelsond975b302016-10-23 12:20:23 -07002973 def get_cced(self):
2974 """Retrieves the list of reviewers."""
2975 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2976 cced = [match.group(2).strip() for match in matches if match]
2977 return cleanup_list(cced)
2978
Nodir Turakulov23b82142017-11-16 11:04:25 -08002979 def get_hash_tags(self):
2980 """Extracts and sanitizes a list of Gerrit hashtags."""
2981 subject = (self._description_lines or ('',))[0]
2982 subject = re.sub(
2983 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2984
2985 tags = []
2986 start = 0
2987 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2988 while True:
2989 m = bracket_exp.match(subject, start)
2990 if not m:
2991 break
2992 tags.append(self.sanitize_hash_tag(m.group(1)))
2993 start = m.end()
2994
2995 if not tags:
2996 # Try "Tag: " prefix.
2997 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
2998 if m:
2999 tags.append(self.sanitize_hash_tag(m.group(1)))
3000 return tags
3001
3002 @classmethod
3003 def sanitize_hash_tag(cls, tag):
3004 """Returns a sanitized Gerrit hash tag.
3005
3006 A sanitized hashtag can be used as a git push refspec parameter value.
3007 """
3008 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3009
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003010 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3011 """Updates this commit description given the parent.
3012
3013 This is essentially what Gnumbd used to do.
3014 Consult https://goo.gl/WMmpDe for more details.
3015 """
3016 assert parent_msg # No, orphan branch creation isn't supported.
3017 assert parent_hash
3018 assert dest_ref
3019 parent_footer_map = git_footers.parse_footers(parent_msg)
3020 # This will also happily parse svn-position, which GnumbD is no longer
3021 # supporting. While we'd generate correct footers, the verifier plugin
3022 # installed in Gerrit will block such commit (ie git push below will fail).
3023 parent_position = git_footers.get_position(parent_footer_map)
3024
3025 # Cherry-picks may have last line obscuring their prior footers,
3026 # from git_footers perspective. This is also what Gnumbd did.
3027 cp_line = None
3028 if (self._description_lines and
3029 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3030 cp_line = self._description_lines.pop()
3031
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003032 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003033
3034 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3035 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003036 for i, line in enumerate(footer_lines):
3037 k, v = git_footers.parse_footer(line) or (None, None)
3038 if k and k.startswith('Cr-'):
3039 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003040
3041 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003042 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003043 if parent_position[0] == dest_ref:
3044 # Same branch as parent.
3045 number = int(parent_position[1]) + 1
3046 else:
3047 number = 1 # New branch, and extra lineage.
3048 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3049 int(parent_position[1])))
3050
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003051 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3052 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003053
3054 self._description_lines = top_lines
3055 if cp_line:
3056 self._description_lines.append(cp_line)
3057 if self._description_lines[-1] != '':
3058 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003059 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003060
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003061
Aaron Gablea1bab272017-04-11 16:38:18 -07003062def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003063 """Retrieves the reviewers that approved a CL from the issue properties with
3064 messages.
3065
3066 Note that the list may contain reviewers that are not committer, thus are not
3067 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003068
3069 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003070 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003071 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003072 return sorted(
3073 set(
3074 message['sender']
3075 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003076 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003077 )
3078 )
3079
3080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081def FindCodereviewSettingsFile(filename='codereview.settings'):
3082 """Finds the given file starting in the cwd and going up.
3083
3084 Only looks up to the top of the repository unless an
3085 'inherit-review-settings-ok' file exists in the root of the repository.
3086 """
3087 inherit_ok_file = 'inherit-review-settings-ok'
3088 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003089 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3091 root = '/'
3092 while True:
3093 if filename in os.listdir(cwd):
3094 if os.path.isfile(os.path.join(cwd, filename)):
3095 return open(os.path.join(cwd, filename))
3096 if cwd == root:
3097 break
3098 cwd = os.path.dirname(cwd)
3099
3100
3101def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003102 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003103 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105 def SetProperty(name, setting, unset_error_ok=False):
3106 fullname = 'rietveld.' + name
3107 if setting in keyvals:
3108 RunGit(['config', fullname, keyvals[setting]])
3109 else:
3110 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3111
tandrii48df5812016-10-17 03:55:37 -07003112 if not keyvals.get('GERRIT_HOST', False):
3113 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114 # Only server setting is required. Other settings can be absent.
3115 # In that case, we ignore errors raised during option deletion attempt.
3116 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3117 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3118 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003119 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003120 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3121 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003122 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3123 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003124 SetProperty(
3125 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003127 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003128 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003129
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003130 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003131 RunGit(['config', 'gerrit.squash-uploads',
3132 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003133
tandrii@chromium.org28253532016-04-14 13:46:56 +00003134 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003135 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003136 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003138 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003139 # should be of the form
3140 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3141 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003142 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3143 keyvals['ORIGIN_URL_CONFIG']])
3144
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003145
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003146def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003147 """Downloads a network object to a local file, like urllib.urlretrieve.
3148
3149 This is necessary because urllib is broken for SSL connections via a proxy.
3150 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003151 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003152 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003153
3154
ukai@chromium.org712d6102013-11-27 00:52:58 +00003155def hasSheBang(fname):
3156 """Checks fname is a #! script."""
3157 with open(fname) as f:
3158 return f.read(2).startswith('#!')
3159
3160
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003161# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3162def DownloadHooks(*args, **kwargs):
3163 pass
3164
3165
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003166def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003167 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003168
3169 Args:
3170 force: True to update hooks. False to install hooks if not present.
3171 """
3172 if not settings.GetIsGerrit():
3173 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003174 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003175 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3176 if not os.access(dst, os.X_OK):
3177 if os.path.exists(dst):
3178 if not force:
3179 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003180 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003181 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003182 if not hasSheBang(dst):
3183 DieWithError('Not a script: %s\n'
3184 'You need to download from\n%s\n'
3185 'into .git/hooks/commit-msg and '
3186 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003187 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3188 except Exception:
3189 if os.path.exists(dst):
3190 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003191 DieWithError('\nFailed to download hooks.\n'
3192 'You need to download from\n%s\n'
3193 'into .git/hooks/commit-msg and '
3194 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003195
3196
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003197class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003198 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003199
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003200 _GOOGLESOURCE = 'googlesource.com'
3201
3202 def __init__(self):
3203 # Cached list of [host, identity, source], where source is either
3204 # .gitcookies or .netrc.
3205 self._all_hosts = None
3206
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003207 def ensure_configured_gitcookies(self):
3208 """Runs checks and suggests fixes to make git use .gitcookies from default
3209 path."""
3210 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3211 configured_path = RunGitSilent(
3212 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003213 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003214 if configured_path:
3215 self._ensure_default_gitcookies_path(configured_path, default)
3216 else:
3217 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003218
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003219 @staticmethod
3220 def _ensure_default_gitcookies_path(configured_path, default_path):
3221 assert configured_path
3222 if configured_path == default_path:
3223 print('git is already configured to use your .gitcookies from %s' %
3224 configured_path)
3225 return
3226
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003227 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003228 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3229 (configured_path, default_path))
3230
3231 if not os.path.exists(configured_path):
3232 print('However, your configured .gitcookies file is missing.')
3233 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3234 action='reconfigure')
3235 RunGit(['config', '--global', 'http.cookiefile', default_path])
3236 return
3237
3238 if os.path.exists(default_path):
3239 print('WARNING: default .gitcookies file already exists %s' %
3240 default_path)
3241 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3242 default_path)
3243
3244 confirm_or_exit('Move existing .gitcookies to default location?',
3245 action='move')
3246 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003247 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003248 print('Moved and reconfigured git to use .gitcookies from %s' %
3249 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003250
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003251 @staticmethod
3252 def _configure_gitcookies_path(default_path):
3253 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3254 if os.path.exists(netrc_path):
3255 print('You seem to be using outdated .netrc for git credentials: %s' %
3256 netrc_path)
3257 print('This tool will guide you through setting up recommended '
3258 '.gitcookies store for git credentials.\n'
3259 '\n'
3260 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3261 ' git config --global --unset http.cookiefile\n'
3262 ' mv %s %s.backup\n\n' % (default_path, default_path))
3263 confirm_or_exit(action='setup .gitcookies')
3264 RunGit(['config', '--global', 'http.cookiefile', default_path])
3265 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003266
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003267 def get_hosts_with_creds(self, include_netrc=False):
3268 if self._all_hosts is None:
3269 a = gerrit_util.CookiesAuthenticator()
3270 self._all_hosts = [
3271 (h, u, s)
3272 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003273 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
3274 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003275 )
3276 if h.endswith(self._GOOGLESOURCE)
3277 ]
3278
3279 if include_netrc:
3280 return self._all_hosts
3281 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3282
3283 def print_current_creds(self, include_netrc=False):
3284 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3285 if not hosts:
3286 print('No Git/Gerrit credentials found')
3287 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003288 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003289 header = [('Host', 'User', 'Which file'),
3290 ['=' * l for l in lengths]]
3291 for row in (header + hosts):
3292 print('\t'.join((('%%+%ds' % l) % s)
3293 for l, s in zip(lengths, row)))
3294
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003295 @staticmethod
3296 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003297 """Parses identity "git-<username>.domain" into <username> and domain."""
3298 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003299 # distinguishable from sub-domains. But we do know typical domains:
3300 if identity.endswith('.chromium.org'):
3301 domain = 'chromium.org'
3302 username = identity[:-len('.chromium.org')]
3303 else:
3304 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003305 if username.startswith('git-'):
3306 username = username[len('git-'):]
3307 return username, domain
3308
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003309 def _canonical_git_googlesource_host(self, host):
3310 """Normalizes Gerrit hosts (with '-review') to Git host."""
3311 assert host.endswith(self._GOOGLESOURCE)
3312 # Prefix doesn't include '.' at the end.
3313 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3314 if prefix.endswith('-review'):
3315 prefix = prefix[:-len('-review')]
3316 return prefix + '.' + self._GOOGLESOURCE
3317
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003318 def _canonical_gerrit_googlesource_host(self, host):
3319 git_host = self._canonical_git_googlesource_host(host)
3320 prefix = git_host.split('.', 1)[0]
3321 return prefix + '-review.' + self._GOOGLESOURCE
3322
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003323 def _get_counterpart_host(self, host):
3324 assert host.endswith(self._GOOGLESOURCE)
3325 git = self._canonical_git_googlesource_host(host)
3326 gerrit = self._canonical_gerrit_googlesource_host(git)
3327 return git if gerrit == host else gerrit
3328
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003329 def has_generic_host(self):
3330 """Returns whether generic .googlesource.com has been configured.
3331
3332 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3333 """
3334 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3335 if host == '.' + self._GOOGLESOURCE:
3336 return True
3337 return False
3338
3339 def _get_git_gerrit_identity_pairs(self):
3340 """Returns map from canonic host to pair of identities (Git, Gerrit).
3341
3342 One of identities might be None, meaning not configured.
3343 """
3344 host_to_identity_pairs = {}
3345 for host, identity, _ in self.get_hosts_with_creds():
3346 canonical = self._canonical_git_googlesource_host(host)
3347 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3348 idx = 0 if canonical == host else 1
3349 pair[idx] = identity
3350 return host_to_identity_pairs
3351
3352 def get_partially_configured_hosts(self):
3353 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003354 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003355 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003356 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003357
3358 def get_conflicting_hosts(self):
3359 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003360 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003361 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003362 if None not in (i1, i2) and i1 != i2)
3363
3364 def get_duplicated_hosts(self):
3365 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003366 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003367
3368 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3369 'chromium.googlesource.com': 'chromium.org',
3370 'chrome-internal.googlesource.com': 'google.com',
3371 }
3372
3373 def get_hosts_with_wrong_identities(self):
3374 """Finds hosts which **likely** reference wrong identities.
3375
3376 Note: skips hosts which have conflicting identities for Git and Gerrit.
3377 """
3378 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003379 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003380 pair = self._get_git_gerrit_identity_pairs().get(host)
3381 if pair and pair[0] == pair[1]:
3382 _, domain = self._parse_identity(pair[0])
3383 if domain != expected:
3384 hosts.add(host)
3385 return hosts
3386
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003387 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003388 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003389 hosts = sorted(hosts)
3390 assert hosts
3391 if extra_column_func is None:
3392 extras = [''] * len(hosts)
3393 else:
3394 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003395 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3396 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003397 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003398 lines.append(tmpl % he)
3399 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003400
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003401 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003402 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003403 yield ('.googlesource.com wildcard record detected',
3404 ['Chrome Infrastructure team recommends to list full host names '
3405 'explicitly.'],
3406 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003407
3408 dups = self.get_duplicated_hosts()
3409 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003410 yield ('The following hosts were defined twice',
3411 self._format_hosts(dups),
3412 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003413
3414 partial = self.get_partially_configured_hosts()
3415 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003416 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3417 'These hosts are missing',
3418 self._format_hosts(partial, lambda host: 'but %s defined' %
3419 self._get_counterpart_host(host)),
3420 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003421
3422 conflicting = self.get_conflicting_hosts()
3423 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003424 yield ('The following Git hosts have differing credentials from their '
3425 'Gerrit counterparts',
3426 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3427 tuple(self._get_git_gerrit_identity_pairs()[host])),
3428 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003429
3430 wrong = self.get_hosts_with_wrong_identities()
3431 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003432 yield ('These hosts likely use wrong identity',
3433 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3434 (self._get_git_gerrit_identity_pairs()[host][0],
3435 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3436 wrong)
3437
3438 def find_and_report_problems(self):
3439 """Returns True if there was at least one problem, else False."""
3440 found = False
3441 bad_hosts = set()
3442 for title, sublines, hosts in self._find_problems():
3443 if not found:
3444 found = True
3445 print('\n\n.gitcookies problem report:\n')
3446 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003447 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003448 if sublines:
3449 print()
3450 print(' %s' % '\n '.join(sublines))
3451 print()
3452
3453 if bad_hosts:
3454 assert found
3455 print(' You can manually remove corresponding lines in your %s file and '
3456 'visit the following URLs with correct account to generate '
3457 'correct credential lines:\n' %
3458 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3459 print(' %s' % '\n '.join(sorted(set(
3460 gerrit_util.CookiesAuthenticator().get_new_password_url(
3461 self._canonical_git_googlesource_host(host))
3462 for host in bad_hosts
3463 ))))
3464 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003465
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003466
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003467@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003468def CMDcreds_check(parser, args):
3469 """Checks credentials and suggests changes."""
3470 _, _ = parser.parse_args(args)
3471
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003472 # Code below checks .gitcookies. Abort if using something else.
3473 authn = gerrit_util.Authenticator.get()
3474 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3475 if isinstance(authn, gerrit_util.GceAuthenticator):
3476 DieWithError(
3477 'This command is not designed for GCE, are you on a bot?\n'
3478 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3479 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003480 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003481 'This command is not designed for bot environment. It checks '
3482 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003483
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003484 checker = _GitCookiesChecker()
3485 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003486
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003487 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003488 checker.print_current_creds(include_netrc=True)
3489
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003490 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003491 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003492 return 0
3493 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003494
3495
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003496@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003497def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003498 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003499 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3500 branch = ShortBranchName(branchref)
3501 _, args = parser.parse_args(args)
3502 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003503 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003504 return RunGit(['config', 'branch.%s.base-url' % branch],
3505 error_ok=False).strip()
3506 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003507 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003508 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3509 error_ok=False).strip()
3510
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003511
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003512def color_for_status(status):
3513 """Maps a Changelist status to color, for CMDstatus and other tools."""
3514 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003515 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003516 'waiting': Fore.BLUE,
3517 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003518 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003519 'lgtm': Fore.GREEN,
3520 'commit': Fore.MAGENTA,
3521 'closed': Fore.CYAN,
3522 'error': Fore.WHITE,
3523 }.get(status, Fore.WHITE)
3524
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003525
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003526def get_cl_statuses(changes, fine_grained, max_processes=None):
3527 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003528
3529 If fine_grained is true, this will fetch CL statuses from the server.
3530 Otherwise, simply indicate if there's a matching url for the given branches.
3531
3532 If max_processes is specified, it is used as the maximum number of processes
3533 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3534 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003535
3536 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003537 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003538 if not changes:
3539 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003540
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003541 if not fine_grained:
3542 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003543 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003544 for cl in changes:
3545 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003546 return
3547
3548 # First, sort out authentication issues.
3549 logging.debug('ensuring credentials exist')
3550 for cl in changes:
3551 cl.EnsureAuthenticated(force=False, refresh=True)
3552
3553 def fetch(cl):
3554 try:
3555 return (cl, cl.GetStatus())
3556 except:
3557 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003558 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003559 raise
3560
3561 threads_count = len(changes)
3562 if max_processes:
3563 threads_count = max(1, min(threads_count, max_processes))
3564 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3565
3566 pool = ThreadPool(threads_count)
3567 fetched_cls = set()
3568 try:
3569 it = pool.imap_unordered(fetch, changes).__iter__()
3570 while True:
3571 try:
3572 cl, status = it.next(timeout=5)
3573 except multiprocessing.TimeoutError:
3574 break
3575 fetched_cls.add(cl)
3576 yield cl, status
3577 finally:
3578 pool.close()
3579
3580 # Add any branches that failed to fetch.
3581 for cl in set(changes) - fetched_cls:
3582 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003583
rmistry@google.com2dd99862015-06-22 12:22:18 +00003584
3585def upload_branch_deps(cl, args):
3586 """Uploads CLs of local branches that are dependents of the current branch.
3587
3588 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003589
3590 test1 -> test2.1 -> test3.1
3591 -> test3.2
3592 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003593
3594 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3595 run on the dependent branches in this order:
3596 test2.1, test3.1, test3.2, test2.2, test3.3
3597
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003598 Note: This function does not rebase your local dependent branches. Use it
3599 when you make a change to the parent branch that will not conflict
3600 with its dependent branches, and you would like their dependencies
3601 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602 """
3603 if git_common.is_dirty_git_tree('upload-branch-deps'):
3604 return 1
3605
3606 root_branch = cl.GetBranch()
3607 if root_branch is None:
3608 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3609 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003610 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003611 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3612 'patchset dependencies without an uploaded CL.')
3613
3614 branches = RunGit(['for-each-ref',
3615 '--format=%(refname:short) %(upstream:short)',
3616 'refs/heads'])
3617 if not branches:
3618 print('No local branches found.')
3619 return 0
3620
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003621 # Create a dictionary of all local branches to the branches that are
3622 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003623 tracked_to_dependents = collections.defaultdict(list)
3624 for b in branches.splitlines():
3625 tokens = b.split()
3626 if len(tokens) == 2:
3627 branch_name, tracked = tokens
3628 tracked_to_dependents[tracked].append(branch_name)
3629
vapiera7fbd5a2016-06-16 09:17:49 -07003630 print()
3631 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003632 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003633
rmistry@google.com2dd99862015-06-22 12:22:18 +00003634 def traverse_dependents_preorder(branch, padding=''):
3635 dependents_to_process = tracked_to_dependents.get(branch, [])
3636 padding += ' '
3637 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003638 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003639 dependents.append(dependent)
3640 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003641
rmistry@google.com2dd99862015-06-22 12:22:18 +00003642 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003643 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003644
3645 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003646 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003647 return 0
3648
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003649 confirm_or_exit('This command will checkout all dependent branches and run '
3650 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003651
rmistry@google.com2dd99862015-06-22 12:22:18 +00003652 # Record all dependents that failed to upload.
3653 failures = {}
3654 # Go through all dependents, checkout the branch and upload.
3655 try:
3656 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003657 print()
3658 print('--------------------------------------')
3659 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003660 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003661 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003662 try:
3663 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003664 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003665 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003666 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003667 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003668 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003669 finally:
3670 # Swap back to the original root branch.
3671 RunGit(['checkout', '-q', root_branch])
3672
vapiera7fbd5a2016-06-16 09:17:49 -07003673 print()
3674 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003675 for dependent_branch in dependents:
3676 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print(' %s : %s' % (dependent_branch, upload_status))
3678 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003679
3680 return 0
3681
3682
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003683def GetArchiveTagForBranch(issue_num, branch_name, existing_tags):
3684 """Given a proposed tag name, returns a tag name that is guaranteed to be
3685 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3686 or 'foo-3', and so on."""
3687
3688 proposed_tag = 'git-cl-archived-%s-%s' % (issue_num, branch_name)
3689 for suffix_num in itertools.count(1):
3690 if suffix_num == 1:
3691 to_check = proposed_tag
3692 else:
3693 to_check = '%s-%d' % (proposed_tag, suffix_num)
3694
3695 if to_check not in existing_tags:
3696 return to_check
3697
3698
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003699@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003700def CMDarchive(parser, args):
3701 """Archives and deletes branches associated with closed changelists."""
3702 parser.add_option(
3703 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003704 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003705 parser.add_option(
3706 '-f', '--force', action='store_true',
3707 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003708 parser.add_option(
3709 '-d', '--dry-run', action='store_true',
3710 help='Skip the branch tagging and removal steps.')
3711 parser.add_option(
3712 '-t', '--notags', action='store_true',
3713 help='Do not tag archived branches. '
3714 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003715
kmarshall3bff56b2016-06-06 18:31:47 -07003716 options, args = parser.parse_args(args)
3717 if args:
3718 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003719
3720 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3721 if not branches:
3722 return 0
3723
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003724 tags = RunGit(['for-each-ref', '--format=%(refname)',
3725 'refs/tags']).splitlines() or []
3726 tags = [t.split('/')[-1] for t in tags]
3727
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003729 changes = [Changelist(branchref=b)
3730 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003731 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3732 statuses = get_cl_statuses(changes,
3733 fine_grained=True,
3734 max_processes=options.maxjobs)
3735 proposal = [(cl.GetBranch(),
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003736 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(),
3737 tags))
kmarshall3bff56b2016-06-06 18:31:47 -07003738 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003739 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003740 proposal.sort()
3741
3742 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003744 return 0
3745
3746 current_branch = GetCurrentBranch()
3747
vapiera7fbd5a2016-06-16 09:17:49 -07003748 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003749 if options.notags:
3750 for next_item in proposal:
3751 print(' ' + next_item[0])
3752 else:
3753 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3754 for next_item in proposal:
3755 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003756
kmarshall9249e012016-08-23 12:02:16 -07003757 # Quit now on precondition failure or if instructed by the user, either
3758 # via an interactive prompt or by command line flags.
3759 if options.dry_run:
3760 print('\nNo changes were made (dry run).\n')
3761 return 0
3762 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003763 print('You are currently on a branch \'%s\' which is associated with a '
3764 'closed codereview issue, so archive cannot proceed. Please '
3765 'checkout another branch and run this command again.' %
3766 current_branch)
3767 return 1
kmarshall9249e012016-08-23 12:02:16 -07003768 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003769 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3770 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003772 return 1
3773
3774 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003775 if not options.notags:
3776 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003777
3778 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3779 # Clean up the tag if we failed to delete the branch.
3780 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003781
vapiera7fbd5a2016-06-16 09:17:49 -07003782 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003783
3784 return 0
3785
3786
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003787@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003788def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003789 """Show status of changelists.
3790
3791 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003792 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003793 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003794 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003795 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003796 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003797 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003798 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003799
3800 Also see 'git cl comments'.
3801 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003802 parser.add_option(
3803 '--no-branch-color',
3804 action='store_true',
3805 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003807 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003808 parser.add_option('-f', '--fast', action='store_true',
3809 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003810 parser.add_option(
3811 '-j', '--maxjobs', action='store', type=int,
3812 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003813 parser.add_option(
3814 '-i', '--issue', type=int,
3815 help='Operate on this issue instead of the current branch\'s implicit '
3816 'issue. Requires --field to be set.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003817 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003818 if args:
3819 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820
iannuccie53c9352016-08-17 14:40:40 -07003821 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003822 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003825 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003827 if cl.GetIssue():
3828 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829 elif options.field == 'id':
3830 issueid = cl.GetIssue()
3831 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003832 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003834 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003835 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003837 elif options.field == 'status':
3838 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003839 elif options.field == 'url':
3840 url = cl.GetIssueURL()
3841 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003842 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003843 return 0
3844
3845 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3846 if not branches:
3847 print('No local branch found.')
3848 return 0
3849
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003850 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003851 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003852 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003854 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003855 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003856 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003857
Daniel McArdlea23bf592019-02-12 00:25:12 +00003858 current_branch = GetCurrentBranch()
3859
3860 def FormatBranchName(branch, colorize=False):
3861 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3862 an asterisk when it is the current branch."""
3863
3864 asterisk = ""
3865 color = Fore.RESET
3866 if branch == current_branch:
3867 asterisk = "* "
3868 color = Fore.GREEN
3869 branch_name = ShortBranchName(branch)
3870
3871 if colorize:
3872 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003873 return asterisk + branch_name
3874
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003875 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003876
3877 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003878 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3879 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003880 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003881 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003882 branch_statuses[c.GetBranch()] = status
3883 status = branch_statuses.pop(branch)
3884 url = cl.GetIssueURL()
3885 if url and (not status or status == 'error'):
3886 # The issue probably doesn't exist anymore.
3887 url += ' (broken)'
3888
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003889 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003890 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003891 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003892 color = ''
3893 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003894 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003895
Alan Cuttera3be9a52019-03-04 18:50:33 +00003896 branch_display = FormatBranchName(branch)
3897 padding = ' ' * (alignment - len(branch_display))
3898 if not options.no_branch_color:
3899 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003900
Alan Cuttera3be9a52019-03-04 18:50:33 +00003901 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3902 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003903
vapiera7fbd5a2016-06-16 09:17:49 -07003904 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003905 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003906 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003907 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003908 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003909 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003910 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003911 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003913 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003914 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003915 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916 return 0
3917
3918
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003919def colorize_CMDstatus_doc():
3920 """To be called once in main() to add colors to git cl status help."""
3921 colors = [i for i in dir(Fore) if i[0].isupper()]
3922
3923 def colorize_line(line):
3924 for color in colors:
3925 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003926 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003927 indent = len(line) - len(line.lstrip(' ')) + 1
3928 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3929 return line
3930
3931 lines = CMDstatus.__doc__.splitlines()
3932 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3933
3934
phajdan.jre328cf92016-08-22 04:12:17 -07003935def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003936 if path == '-':
3937 json.dump(contents, sys.stdout)
3938 else:
3939 with open(path, 'w') as f:
3940 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003941
3942
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003943@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003944@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003945def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003946 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947
3948 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003949 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003950 parser.add_option('-r', '--reverse', action='store_true',
3951 help='Lookup the branch(es) for the specified issues. If '
3952 'no issues are specified, all branches with mapped '
3953 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003954 parser.add_option('--json',
3955 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003956 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957
dnj@chromium.org406c4402015-03-03 17:22:28 +00003958 if options.reverse:
3959 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003960 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003961 # Reverse issue lookup.
3962 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003963
3964 git_config = {}
3965 for config in RunGit(['config', '--get-regexp',
3966 r'branch\..*issue']).splitlines():
3967 name, _space, val = config.partition(' ')
3968 git_config[name] = val
3969
dnj@chromium.org406c4402015-03-03 17:22:28 +00003970 for branch in branches:
Edward Lemur52969c92020-02-06 18:15:28 +00003971 config_key = _git_branch_config_key(ShortBranchName(branch),
3972 Changelist.IssueConfigKey())
3973 issue = git_config.get(config_key)
3974 if issue:
3975 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003976 if not args:
3977 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003978 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003979 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003980 try:
3981 issue_num = int(issue)
3982 except ValueError:
3983 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003984 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003985 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003987 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003988 if options.json:
3989 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003990 return 0
3991
3992 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003993 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003994 if not issue.valid:
3995 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3996 'or no argument to list it.\n'
3997 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003998 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003999 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004000 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004001 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004002 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4003 if options.json:
4004 write_json(options.json, {
4005 'issue': cl.GetIssue(),
4006 'issue_url': cl.GetIssueURL(),
4007 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004008 return 0
4009
4010
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004011@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004012def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004013 """Shows or posts review comments for any changelist."""
4014 parser.add_option('-a', '--add-comment', dest='comment',
4015 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004016 parser.add_option('-p', '--publish', action='store_true',
4017 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004018 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004019 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004020 parser.add_option('-m', '--machine-readable', dest='readable',
4021 action='store_false', default=True,
4022 help='output comments in a format compatible with '
4023 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004024 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004025 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004026 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004027
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004028 issue = None
4029 if options.issue:
4030 try:
4031 issue = int(options.issue)
4032 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004033 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004034
Edward Lemur934836a2019-09-09 20:16:54 +00004035 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004036
4037 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004038 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004039 return 0
4040
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004041 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4042 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004043 for comment in summary:
4044 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004045 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004046 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004047 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004048 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004049 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004050 elif comment.autogenerated:
4051 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004052 else:
4053 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004054 print('\n%s%s %s%s\n%s' % (
4055 color,
4056 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4057 comment.sender,
4058 Fore.RESET,
4059 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4060
smut@google.comc85ac942015-09-15 16:34:43 +00004061 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004062 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00004063 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004064 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4065 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00004066 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004067 return 0
4068
4069
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004070@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004071@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004072def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004073 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004074 parser.add_option('-d', '--display', action='store_true',
4075 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004076 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004077 help='New description to set for this issue (- for stdin, '
4078 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004079 parser.add_option('-f', '--force', action='store_true',
4080 help='Delete any unpublished Gerrit edits for this issue '
4081 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004082
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004083 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004084
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004085 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004086 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004087 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004088 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004089 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004090
Edward Lemur934836a2019-09-09 20:16:54 +00004091 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004092 if target_issue_arg:
4093 kwargs['issue'] = target_issue_arg.issue
4094 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004095
4096 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004097 if not cl.GetIssue():
4098 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004099
Edward Lemur678a6842019-10-03 22:25:05 +00004100 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004101 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004102
Edward Lemur6c6827c2020-02-06 21:15:18 +00004103 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004104
smut@google.com34fb6b12015-07-13 20:03:26 +00004105 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004107 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004108
4109 if options.new_description:
4110 text = options.new_description
4111 if text == '-':
4112 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004113 elif text == '+':
4114 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemur7f6dec02020-02-06 20:23:58 +00004115 text = cl.GetLocalDescription(base_branch)
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004116
4117 description.set_description(text)
4118 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004119 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00004120 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004121 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004122 return 0
4123
4124
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004125@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004126def CMDlint(parser, args):
4127 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004128 parser.add_option('--filter', action='append', metavar='-x,+y',
4129 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004130 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004131
4132 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004133 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004134 try:
4135 import cpplint
4136 import cpplint_chromium
4137 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004138 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004139 return 1
4140
4141 # Change the current working directory before calling lint so that it
4142 # shows the correct base.
4143 previous_cwd = os.getcwd()
4144 os.chdir(settings.GetRoot())
4145 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004146 cl = Changelist()
Edward Lemur7f6dec02020-02-06 20:23:58 +00004147 change = cl.GetChange(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org44202a22014-03-11 19:22:18 +00004148 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004149 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004150 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004151 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004152
4153 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004154 command = args + files
4155 if options.filter:
4156 command = ['--filter=' + ','.join(options.filter)] + command
4157 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004158
4159 white_regex = re.compile(settings.GetLintRegex())
4160 black_regex = re.compile(settings.GetLintIgnoreRegex())
4161 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4162 for filename in filenames:
4163 if white_regex.match(filename):
4164 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004165 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004166 else:
4167 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4168 extra_check_functions)
4169 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004171 finally:
4172 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004173 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004174 if cpplint._cpplint_state.error_count != 0:
4175 return 1
4176 return 0
4177
4178
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004179@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004181 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004182 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004183 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004184 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004185 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004186 parser.add_option('--all', action='store_true',
4187 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004188 parser.add_option('--parallel', action='store_true',
4189 help='Run all tests specified by input_api.RunTests in all '
4190 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004191 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004192
sbc@chromium.org71437c02015-04-09 19:29:40 +00004193 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004194 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195 return 1
4196
Edward Lemur934836a2019-09-09 20:16:54 +00004197 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198 if args:
4199 base_branch = args[0]
4200 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004201 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004202 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203
Aaron Gable8076c282017-11-29 14:39:41 -08004204 if options.all:
Edward Lemur7f6dec02020-02-06 20:23:58 +00004205 base_change = cl.GetChange(base_branch)
Aaron Gable8076c282017-11-29 14:39:41 -08004206 files = [('M', f) for f in base_change.AllFiles()]
4207 change = presubmit_support.GitChange(
4208 base_change.Name(),
4209 base_change.FullDescriptionText(),
4210 base_change.RepositoryRoot(),
4211 files,
4212 base_change.issue,
4213 base_change.patchset,
4214 base_change.author_email,
4215 base_change._upstream)
4216 else:
Edward Lemur7f6dec02020-02-06 20:23:58 +00004217 change = cl.GetChange(base_branch)
Aaron Gable8076c282017-11-29 14:39:41 -08004218
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004219 cl.RunHook(
4220 committing=not options.upload,
4221 may_prompt=False,
4222 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004223 change=change,
4224 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004225 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226
4227
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004228def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004229 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004230
4231 Works the same way as
4232 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4233 but can be called on demand on all platforms.
4234
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004235 The basic idea is to generate git hash of a state of the tree, original
4236 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004237 """
4238 lines = []
4239 tree_hash = RunGitSilent(['write-tree'])
4240 lines.append('tree %s' % tree_hash.strip())
4241 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4242 if code == 0:
4243 lines.append('parent %s' % parent.strip())
4244 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4245 lines.append('author %s' % author.strip())
4246 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4247 lines.append('committer %s' % committer.strip())
4248 lines.append('')
4249 # Note: Gerrit's commit-hook actually cleans message of some lines and
4250 # whitespace. This code is not doing this, but it clearly won't decrease
4251 # entropy.
4252 lines.append(message)
4253 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004254 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004255 return 'I%s' % change_hash.strip()
4256
4257
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004258def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004259 """Computes the remote branch ref to use for the CL.
4260
4261 Args:
4262 remote (str): The git remote for the CL.
4263 remote_branch (str): The git remote branch for the CL.
4264 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004265 """
4266 if not (remote and remote_branch):
4267 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004268
wittman@chromium.org455dc922015-01-26 20:15:50 +00004269 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004270 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004271 # refs, which are then translated into the remote full symbolic refs
4272 # below.
4273 if '/' not in target_branch:
4274 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4275 else:
4276 prefix_replacements = (
4277 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4278 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4279 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4280 )
4281 match = None
4282 for regex, replacement in prefix_replacements:
4283 match = re.search(regex, target_branch)
4284 if match:
4285 remote_branch = target_branch.replace(match.group(0), replacement)
4286 break
4287 if not match:
4288 # This is a branch path but not one we recognize; use as-is.
4289 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004290 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4291 # Handle the refs that need to land in different refs.
4292 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004293
wittman@chromium.org455dc922015-01-26 20:15:50 +00004294 # Create the true path to the remote branch.
4295 # Does the following translation:
4296 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4297 # * refs/remotes/origin/master -> refs/heads/master
4298 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4299 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4300 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4301 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4302 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4303 'refs/heads/')
4304 elif remote_branch.startswith('refs/remotes/branch-heads'):
4305 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004306
wittman@chromium.org455dc922015-01-26 20:15:50 +00004307 return remote_branch
4308
4309
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004310def cleanup_list(l):
4311 """Fixes a list so that comma separated items are put as individual items.
4312
4313 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4314 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4315 """
4316 items = sum((i.split(',') for i in l), [])
4317 stripped_items = (i.strip() for i in items)
4318 return sorted(filter(None, stripped_items))
4319
4320
Aaron Gable4db38df2017-11-03 14:59:07 -07004321@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004322@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004323def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004324 """Uploads the current changelist to codereview.
4325
4326 Can skip dependency patchset uploads for a branch by running:
4327 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004328 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004329 git config --unset branch.branch_name.skip-deps-uploads
4330 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004331
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004332 If the name of the checked out branch starts with "bug-" or "fix-" followed
4333 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004334 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004335
4336 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004337 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004338 [git-cl] add support for hashtags
4339 Foo bar: implement foo
4340 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004341 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004342 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4343 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004344 parser.add_option('--bypass-watchlists', action='store_true',
4345 dest='bypass_watchlists',
4346 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004347 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004348 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004349 parser.add_option('--message', '-m', dest='message',
4350 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004351 parser.add_option('-b', '--bug',
4352 help='pre-populate the bug number(s) for this issue. '
4353 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004354 parser.add_option('--message-file', dest='message_file',
4355 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004356 parser.add_option('--title', '-t', dest='title',
4357 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004358 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004359 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004360 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004361 parser.add_option('--tbrs',
4362 action='append', default=[],
4363 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004364 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004365 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004366 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004367 parser.add_option('--hashtag', dest='hashtags',
4368 action='append', default=[],
4369 help=('Gerrit hashtag for new CL; '
4370 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004371 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004372 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004373 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004374 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004375 metavar='TARGET',
4376 help='Apply CL to remote ref TARGET. ' +
4377 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004378 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004379 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004380 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004381 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004382 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004383 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004384 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4385 const='TBR', help='add a set of OWNERS to TBR')
4386 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4387 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004388 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004389 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004390 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004391 'implies --send-mail')
4392 parser.add_option('-d', '--cq-dry-run',
4393 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004394 help='Send the patchset to do a CQ dry run right after '
4395 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004396 parser.add_option('--preserve-tryjobs', action='store_true',
4397 help='instruct the CQ to let tryjobs running even after '
4398 'new patchsets are uploaded instead of canceling '
4399 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004400 parser.add_option('--dependencies', action='store_true',
4401 help='Uploads CLs of all the local branches that depend on '
4402 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004403 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4404 help='Sends your change to the CQ after an approval. Only '
4405 'works on repos that have the Auto-Submit label '
4406 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004407 parser.add_option('--parallel', action='store_true',
4408 help='Run all tests specified by input_api.RunTests in all '
4409 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004410 parser.add_option('--no-autocc', action='store_true',
4411 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004412 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004413 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004414 parser.add_option('-R', '--retry-failed', action='store_true',
4415 help='Retry failed tryjobs from old patchset immediately '
4416 'after uploading new patchset. Cannot be used with '
4417 '--use-commit-queue or --cq-dry-run.')
4418 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4419 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004420 parser.add_option('--fixed', '-x',
4421 help='List of bugs that will be commented on and marked '
4422 'fixed (pre-populates "Fixed:" tag). Same format as '
4423 '-b option / "Bug:" tag. If fixing several issues, '
4424 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004425 parser.add_option('--edit-description', action='store_true', default=False,
4426 help='Modify description before upload. Cannot be used '
4427 'with --force. It is a noop when --no-squash is set '
4428 'or a new commit is created.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004429
rmistry@google.com2dd99862015-06-22 12:22:18 +00004430 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004431 (options, args) = parser.parse_args(args)
4432
sbc@chromium.org71437c02015-04-09 19:29:40 +00004433 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004434 return 1
4435
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004436 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004437 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004438 options.cc = cleanup_list(options.cc)
4439
Josipe827b0f2020-01-30 00:07:20 +00004440 if options.edit_description and options.force:
4441 parser.error('Only one of --force and --edit-description allowed')
4442
tandriib80458a2016-06-23 12:20:07 -07004443 if options.message_file:
4444 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004445 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004446 options.message = gclient_utils.FileRead(options.message_file)
4447 options.message_file = None
4448
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004449 if ([options.cq_dry_run,
4450 options.use_commit_queue,
4451 options.retry_failed].count(True) > 1):
4452 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4453 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004454
Aaron Gableedbc4132017-09-11 13:22:28 -07004455 if options.use_commit_queue:
4456 options.send_mail = True
4457
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004458 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4459 settings.GetIsGerrit()
4460
Edward Lemur934836a2019-09-09 20:16:54 +00004461 cl = Changelist()
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004462 if options.retry_failed and not cl.GetIssue():
4463 print('No previous patchsets, so --retry-failed has no effect.')
4464 options.retry_failed = False
4465 # cl.GetMostRecentPatchset uses cached information, and can return the last
4466 # patchset before upload. Calling it here makes it clear that it's the
4467 # last patchset before upload. Note that GetMostRecentPatchset will fail
4468 # if no CL has been uploaded yet.
4469 if options.retry_failed:
4470 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004471
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004472 ret = cl.CMDUpload(options, args, orig_args)
4473
4474 if options.retry_failed:
4475 if ret != 0:
4476 print('Upload failed, so --retry-failed has no effect.')
4477 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004478 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004479 cl, options.buildbucket_host, latest_patchset=patchset)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +00004480 buckets = _filter_failed_for_retry(builds)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004481 if len(buckets) == 0:
4482 print('No failed tryjobs, so --retry-failed has no effect.')
4483 return ret
Edward Lemur5b929a42019-10-21 17:57:39 +00004484 _trigger_try_jobs(cl, buckets, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004485
4486 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004487
4488
Francois Dorayd42c6812017-05-30 15:10:20 -04004489@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004490@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004491def CMDsplit(parser, args):
4492 """Splits a branch into smaller branches and uploads CLs.
4493
4494 Creates a branch and uploads a CL for each group of files modified in the
4495 current branch that share a common OWNERS file. In the CL description and
Yannic Bonenberger68409632020-01-23 18:29:01 +00004496 comment, '$directory' is replaced with the directory containing the changes
4497 in this CL, '$cl_index' is replaced with the index of the CL we're currently
4498 sending out, and '$num_cls' is replaced with the total number of CLs that
4499 we're sending out in this split.
Francois Dorayd42c6812017-05-30 15:10:20 -04004500 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004501 parser.add_option('-d', '--description', dest='description_file',
4502 help='A text file containing a CL description in which '
4503 '$directory will be replaced by each CL\'s directory.')
4504 parser.add_option('-c', '--comment', dest='comment_file',
4505 help='A text file containing a CL comment.')
4506 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004507 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004508 help='List the files and reviewers for each CL that would '
4509 'be created, but don\'t create branches or CLs.')
4510 parser.add_option('--cq-dry-run', action='store_true',
4511 help='If set, will do a cq dry run for each uploaded CL. '
4512 'Please be careful when doing this; more than ~10 CLs '
4513 'has the potential to overload our build '
4514 'infrastructure. Try to upload these not during high '
4515 'load times (usually 11-3 Mountain View time). Email '
4516 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004517 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4518 default=True,
4519 help='Sends your change to the CQ after an approval. Only '
4520 'works on repos that have the Auto-Submit label '
4521 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004522 options, _ = parser.parse_args(args)
4523
4524 if not options.description_file:
4525 parser.error('No --description flag specified.')
4526
4527 def WrappedCMDupload(args):
4528 return CMDupload(OptionParser(), args)
4529
4530 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004531 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004532 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004533
4534
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004535@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004536@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004538 """DEPRECATED: Used to commit the current changelist via git-svn."""
4539 message = ('git-cl no longer supports committing to SVN repositories via '
4540 'git-svn. You probably want to use `git cl land` instead.')
4541 print(message)
4542 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543
4544
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004545@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004546@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004547def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004548 """Commits the current changelist via git.
4549
4550 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4551 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004552 """
4553 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4554 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004555 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004556 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004557 parser.add_option('--parallel', action='store_true',
4558 help='Run all tests specified by input_api.RunTests in all '
4559 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004560 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004561
Edward Lemur934836a2019-09-09 20:16:54 +00004562 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004563
Robert Iannucci2e73d432018-03-14 01:10:47 -07004564 if not cl.GetIssue():
4565 DieWithError('You must upload the change first to Gerrit.\n'
4566 ' If you would rather have `git cl land` upload '
4567 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004568 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004569 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004570
4571
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004572@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004573@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004574def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004575 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004576 parser.add_option('-b', dest='newbranch',
4577 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004578 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004579 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004580 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004581 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004582
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004583 group = optparse.OptionGroup(
4584 parser,
4585 'Options for continuing work on the current issue uploaded from a '
4586 'different clone (e.g. different machine). Must be used independently '
4587 'from the other options. No issue number should be specified, and the '
4588 'branch must have an issue number associated with it')
4589 group.add_option('--reapply', action='store_true', dest='reapply',
4590 help='Reset the branch and reapply the issue.\n'
4591 'CAUTION: This will undo any local changes in this '
4592 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004593
4594 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004595 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004596 parser.add_option_group(group)
4597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004598 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004599
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004600 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004601 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004602 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004603 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004604 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004605
Edward Lemur934836a2019-09-09 20:16:54 +00004606 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004607 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004608 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004609
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004610 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004611 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004612 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004613
4614 RunGit(['reset', '--hard', upstream])
4615 if options.pull:
4616 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004617
Edward Lemur678a6842019-10-03 22:25:05 +00004618 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4619 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004620
4621 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004622 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004623
Edward Lemurf38bc172019-09-03 21:02:13 +00004624 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004625 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004626 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004627
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004628 # We don't want uncommitted changes mixed up with the patch.
4629 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004630 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004631
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004632 if options.newbranch:
4633 if options.force:
4634 RunGit(['branch', '-D', options.newbranch],
4635 stderr=subprocess2.PIPE, error_ok=True)
4636 RunGit(['new-branch', options.newbranch])
4637
Edward Lemur678a6842019-10-03 22:25:05 +00004638 cl = Changelist(
4639 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004640
Edward Lemur678a6842019-10-03 22:25:05 +00004641 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004642 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004643
Edward Lemurf38bc172019-09-03 21:02:13 +00004644 return cl.CMDPatchWithParsedIssue(
4645 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646
4647
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004648def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004649 """Fetches the tree status and returns either 'open', 'closed',
4650 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004651 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004652 if url:
Edward Lemur79d4f992019-11-11 23:49:02 +00004653 status = urllib.request.urlopen(url).read().lower()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004654 if status.find('closed') != -1 or status == '0':
4655 return 'closed'
4656 elif status.find('open') != -1 or status == '1':
4657 return 'open'
4658 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004659 return 'unset'
4660
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662def GetTreeStatusReason():
4663 """Fetches the tree status from a json url and returns the message
4664 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004665 url = settings.GetTreeStatusUrl()
4666 json_url = urlparse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004667 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004668 status = json.loads(connection.read())
4669 connection.close()
4670 return status['message']
4671
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004672
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004673@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004674def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004675 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004676 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004677 status = GetTreeStatus()
4678 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004679 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680 return 2
4681
vapiera7fbd5a2016-06-16 09:17:49 -07004682 print('The tree is %s' % status)
4683 print()
4684 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004685 if status != 'open':
4686 return 1
4687 return 0
4688
4689
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004690@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004691def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004692 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4693 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004694 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004695 '-b', '--bot', action='append',
4696 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4697 'times to specify multiple builders. ex: '
4698 '"-b win_rel -b win_layout". See '
4699 'the try server waterfall for the builders name and the tests '
4700 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004701 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004702 '-B', '--bucket', default='',
4703 help=('Buildbucket bucket to send the try requests.'))
4704 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004705 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004706 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004707 'be determined by the try recipe that builder runs, which usually '
4708 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004709 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004710 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004711 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004712 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004713 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004714 '--category', default='git_cl_try', help='Specify custom build category.')
4715 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004716 '--project',
4717 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004718 'in recipe to determine to which repository or directory to '
4719 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004720 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004721 '-p', '--property', dest='properties', action='append', default=[],
4722 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004723 'key2=value2 etc. The value will be treated as '
4724 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004725 'NOTE: using this may make your tryjob not usable for CQ, '
4726 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004727 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004728 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4729 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004730 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004731 parser.add_option(
4732 '-R', '--retry-failed', action='store_true', default=False,
4733 help='Retry failed jobs from the latest set of tryjobs. '
4734 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004735 parser.add_option(
4736 '-i', '--issue', type=int,
4737 help='Operate on this issue instead of the current branch\'s implicit '
4738 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004739 options, args = parser.parse_args(args)
4740
machenbach@chromium.org45453142015-09-15 08:45:22 +00004741 # Make sure that all properties are prop=value pairs.
4742 bad_params = [x for x in options.properties if '=' not in x]
4743 if bad_params:
4744 parser.error('Got properties with missing "=": %s' % bad_params)
4745
maruel@chromium.org15192402012-09-06 12:38:29 +00004746 if args:
4747 parser.error('Unknown arguments: %s' % args)
4748
Edward Lemur934836a2019-09-09 20:16:54 +00004749 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004750 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004751 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004752
Edward Lemurf38bc172019-09-03 21:02:13 +00004753 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004754 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004755
tandriie113dfd2016-10-11 10:20:12 -07004756 error_message = cl.CannotTriggerTryJobReason()
4757 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004758 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004759
Quinten Yearsley983111f2019-09-26 17:18:48 +00004760 if options.retry_failed:
4761 if options.bot or options.bucket:
4762 print('ERROR: The option --retry-failed is not compatible with '
4763 '-B, -b, --bucket, or --bot.', file=sys.stderr)
4764 return 1
4765 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004766 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004767 if options.verbose:
4768 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +00004769 buckets = _filter_failed_for_retry(builds)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004770 if not buckets:
4771 print('There are no failed jobs in the latest set of jobs '
4772 '(patchset #%d), doing nothing.' % patchset)
4773 return 0
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00004774 num_builders = sum(map(len, buckets.values()))
Quinten Yearsley983111f2019-09-26 17:18:48 +00004775 if num_builders > 10:
4776 confirm_or_exit('There are %d builders with failed builds.'
4777 % num_builders, action='continue')
4778 else:
4779 buckets = _get_bucket_map(cl, options, parser)
4780 if buckets and any(b.startswith('master.') for b in buckets):
4781 print('ERROR: Buildbot masters are not supported.')
4782 return 1
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004783
qyearsleydd49f942016-10-28 11:57:22 -07004784 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4785 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004786 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004787 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004788 print('git cl try with no bots now defaults to CQ dry run.')
4789 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4790 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004791
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00004792 for builders in buckets.values():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004793 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004794 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004795 'of bot requires an initial job from a parent (usually a builder). '
4796 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004797 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004798 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004799
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004800 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004801 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00004802 _trigger_try_jobs(cl, buckets, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004803 except BuildbucketResponseException as ex:
4804 print('ERROR: %s' % ex)
4805 return 1
4806 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004807
4808
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004809@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004810def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004811 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004812 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004813 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004814 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004815 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004816 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004817 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004818 '--color', action='store_true', default=setup_color.IS_TTY,
4819 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004820 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004821 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4822 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004823 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004824 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004825 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004826 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00004827 parser.add_option(
4828 '-i', '--issue', type=int,
4829 help='Operate on this issue instead of the current branch\'s implicit '
4830 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004831 options, args = parser.parse_args(args)
4832 if args:
4833 parser.error('Unrecognized args: %s' % ' '.join(args))
4834
Edward Lemur934836a2019-09-09 20:16:54 +00004835 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004836 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004837 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004838
tandrii221ab252016-10-06 08:12:04 -07004839 patchset = options.patchset
4840 if not patchset:
4841 patchset = cl.GetMostRecentPatchset()
4842 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004843 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004844 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004845 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004846 cl.GetIssue())
4847
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004848 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00004849 jobs = fetch_try_jobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004850 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004851 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004852 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004853 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004854 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004855 else:
4856 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004857 return 0
4858
4859
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004860@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004861@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004862def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004863 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004864 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004865 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004866 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004867
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004868 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004869 if args:
4870 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004871 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004872 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004873 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004874 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004875
4876 # Clear configured merge-base, if there is one.
4877 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004878 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004879 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004880 return 0
4881
4882
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004883@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004884def CMDweb(parser, args):
4885 """Opens the current CL in the web browser."""
4886 _, args = parser.parse_args(args)
4887 if args:
4888 parser.error('Unrecognized args: %s' % ' '.join(args))
4889
4890 issue_url = Changelist().GetIssueURL()
4891 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004892 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004893 return 1
4894
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004895 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004896 # allows us to hide the "Created new window in existing browser session."
4897 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004898 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004899 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004900 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004901 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004902 os.open(os.devnull, os.O_RDWR)
4903 try:
4904 webbrowser.open(issue_url)
4905 finally:
4906 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004907 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004908 return 0
4909
4910
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004911@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004912def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004913 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004914 parser.add_option('-d', '--dry-run', action='store_true',
4915 help='trigger in dry run mode')
4916 parser.add_option('-c', '--clear', action='store_true',
4917 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00004918 parser.add_option(
4919 '-i', '--issue', type=int,
4920 help='Operate on this issue instead of the current branch\'s implicit '
4921 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004922 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004923 if args:
4924 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004925 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004926 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004927
Edward Lemur934836a2019-09-09 20:16:54 +00004928 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004929 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004930 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004931 elif options.dry_run:
4932 state = _CQState.DRY_RUN
4933 else:
4934 state = _CQState.COMMIT
4935 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004936 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004937 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004938 return 0
4939
4940
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004941@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004942def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004943 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00004944 parser.add_option(
4945 '-i', '--issue', type=int,
4946 help='Operate on this issue instead of the current branch\'s implicit '
4947 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004948 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004949 if args:
4950 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004951 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004952 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004953 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004954 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004955 cl.CloseIssue()
4956 return 0
4957
4958
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004959@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004960def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004961 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004962 parser.add_option(
4963 '--stat',
4964 action='store_true',
4965 dest='stat',
4966 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004967 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004968 if args:
4969 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004970
Edward Lemur934836a2019-09-09 20:16:54 +00004971 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004972 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004973 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004974 if not issue:
4975 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004976
Aaron Gablea718c3e2017-08-28 17:47:28 -07004977 base = cl._GitGetBranchConfigValue('last-upload-hash')
4978 if not base:
4979 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4980 if not base:
4981 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4982 revision_info = detail['revisions'][detail['current_revision']]
4983 fetch_info = revision_info['fetch']['http']
4984 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4985 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004986
Aaron Gablea718c3e2017-08-28 17:47:28 -07004987 cmd = ['git', 'diff']
4988 if options.stat:
4989 cmd.append('--stat')
4990 cmd.append(base)
4991 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004992
4993 return 0
4994
4995
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004996@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004997def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004998 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004999 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005000 '--ignore-current',
5001 action='store_true',
5002 help='Ignore the CL\'s current reviewers and start from scratch.')
5003 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005004 '--ignore-self',
5005 action='store_true',
5006 help='Do not consider CL\'s author as an owners.')
5007 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005008 '--no-color',
5009 action='store_true',
5010 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005011 parser.add_option(
5012 '--batch',
5013 action='store_true',
5014 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005015 # TODO: Consider moving this to another command, since other
5016 # git-cl owners commands deal with owners for a given CL.
5017 parser.add_option(
5018 '--show-all',
5019 action='store_true',
5020 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005021 options, args = parser.parse_args(args)
5022
5023 author = RunGit(['config', 'user.email']).strip() or None
5024
Edward Lemur934836a2019-09-09 20:16:54 +00005025 cl = Changelist()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005026
Yang Guo6e269a02019-06-26 11:17:02 +00005027 if options.show_all:
5028 for arg in args:
5029 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemur7f6dec02020-02-06 20:23:58 +00005030 database = owners.Database(settings.GetRoot(), file, os.path)
Yang Guo6e269a02019-06-26 11:17:02 +00005031 database.load_data_needed_for([arg])
5032 print('Owners for %s:' % arg)
5033 for owner in sorted(database.all_possible_owners([arg], None)):
5034 print(' - %s' % owner)
5035 return 0
5036
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005037 if args:
5038 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005039 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005040 base_branch = args[0]
5041 else:
5042 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005043 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005044
Edward Lemur7f6dec02020-02-06 20:23:58 +00005045 change = cl.GetChange(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07005046 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5047
5048 if options.batch:
5049 db = owners.Database(change.RepositoryRoot(), file, os.path)
5050 print('\n'.join(db.reviewers_for(affected_files, author)))
5051 return 0
5052
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005053 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005054 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005055 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005056 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005057 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005058 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005059 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005060 override_files=change.OriginalOwnersFiles(),
5061 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005062
5063
Aiden Bennerc08566e2018-10-03 17:52:42 +00005064def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005065 """Generates a diff command."""
5066 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005067 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5068
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005069 if allow_prefix:
5070 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5071 # case that diff.noprefix is set in the user's git config.
5072 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5073 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005074 diff_cmd += ['--no-prefix']
5075
5076 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005077
5078 if args:
5079 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005080 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005081 diff_cmd.append(arg)
5082 else:
5083 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005084
5085 return diff_cmd
5086
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005087
Jamie Madill5e96ad12020-01-13 16:08:35 +00005088def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
5089 """Runs clang-format-diff and sets a return value if necessary."""
5090
5091 if not clang_diff_files:
5092 return 0
5093
5094 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5095 # formatted. This is used to block during the presubmit.
5096 return_value = 0
5097
5098 # Locate the clang-format binary in the checkout
5099 try:
5100 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
5101 except clang_format.NotFoundError as e:
5102 DieWithError(e)
5103
5104 if opts.full or settings.GetFormatFullByDefault():
5105 cmd = [clang_format_tool]
5106 if not opts.dry_run and not opts.diff:
5107 cmd.append('-i')
5108 if opts.dry_run:
5109 for diff_file in clang_diff_files:
5110 with open(diff_file, 'r') as myfile:
5111 code = myfile.read().replace('\r\n', '\n')
5112 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5113 stdout = stdout.replace('\r\n', '\n')
5114 if opts.diff:
5115 sys.stdout.write(stdout)
5116 if code != stdout:
5117 return_value = 2
5118 else:
5119 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5120 if opts.diff:
5121 sys.stdout.write(stdout)
5122 else:
5123 env = os.environ.copy()
5124 env['PATH'] = str(os.path.dirname(clang_format_tool))
5125 try:
5126 script = clang_format.FindClangFormatScriptInChromiumTree(
5127 'clang-format-diff.py')
5128 except clang_format.NotFoundError as e:
5129 DieWithError(e)
5130
5131 cmd = [sys.executable, script, '-p0']
5132 if not opts.dry_run and not opts.diff:
5133 cmd.append('-i')
5134
5135 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5136 diff_output = RunGit(diff_cmd)
5137
5138 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5139 if opts.diff:
5140 sys.stdout.write(stdout)
5141 if opts.dry_run and len(stdout) > 0:
5142 return_value = 2
5143
5144 return return_value
5145
5146
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005147def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005148 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005149 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005150
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005151
enne@chromium.org555cfe42014-01-29 18:21:39 +00005152@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005153@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005154def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005155 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005156 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005157 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005158 parser.add_option('--full', action='store_true',
5159 help='Reformat the full content of all touched files')
5160 parser.add_option('--dry-run', action='store_true',
5161 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005162 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005163 '--no-clang-format',
5164 dest='clang_format',
5165 action='store_false',
5166 default=True,
5167 help='Disables formatting of various file types using clang-format.')
5168 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005169 '--python',
5170 action='store_true',
5171 default=None,
5172 help='Enables python formatting on all python files.')
5173 parser.add_option(
5174 '--no-python',
5175 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005176 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005177 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005178 'If neither --python or --no-python are set, python files that have a '
5179 '.style.yapf file in an ancestor directory will be formatted. '
5180 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005181 parser.add_option(
5182 '--js',
5183 action='store_true',
5184 help='Format javascript code with clang-format. '
5185 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005186 parser.add_option('--diff', action='store_true',
5187 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005188 parser.add_option('--presubmit', action='store_true',
5189 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005190 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005191
Garrett Beaty91a6f332020-01-06 16:57:24 +00005192 if opts.python is not None and opts.no_python:
5193 raise parser.error('Cannot set both --python and --no-python')
5194 if opts.no_python:
5195 opts.python = False
5196
Daniel Chengc55eecf2016-12-30 03:11:02 -08005197 # Normalize any remaining args against the current path, so paths relative to
5198 # the current directory are still resolved as expected.
5199 args = [os.path.join(os.getcwd(), arg) for arg in args]
5200
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005201 # git diff generates paths against the root of the repository. Change
5202 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005203 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005204 if rel_base_path:
5205 os.chdir(rel_base_path)
5206
digit@chromium.org29e47272013-05-17 17:01:46 +00005207 # Grab the merge-base commit, i.e. the upstream commit of the current
5208 # branch when it was created or the last time it was rebased. This is
5209 # to cover the case where the user may have called "git fetch origin",
5210 # moving the origin branch to a newer commit, but hasn't rebased yet.
5211 upstream_commit = None
5212 cl = Changelist()
5213 upstream_branch = cl.GetUpstreamBranch()
5214 if upstream_branch:
5215 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5216 upstream_commit = upstream_commit.strip()
5217
5218 if not upstream_commit:
5219 DieWithError('Could not find base commit for this branch. '
5220 'Are you in detached state?')
5221
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005222 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5223 diff_output = RunGit(changed_files_cmd)
5224 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005225 # Filter out files deleted by this CL
5226 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005227
Andreas Haas417d89c2020-02-06 10:24:27 +00005228 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005229 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005230
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005231 clang_diff_files = []
5232 if opts.clang_format:
5233 clang_diff_files = [
5234 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5235 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005236 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5237 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005238 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005239
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005240 top_dir = os.path.normpath(
5241 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5242
Jamie Madill5e96ad12020-01-13 16:08:35 +00005243 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5244 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005245
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005246 # Similar code to above, but using yapf on .py files rather than clang-format
5247 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005248 py_explicitly_disabled = opts.python is not None and not opts.python
5249 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005250 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5251 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5252 if sys.platform.startswith('win'):
5253 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005254
Aiden Bennerc08566e2018-10-03 17:52:42 +00005255 # Used for caching.
5256 yapf_configs = {}
5257 for f in python_diff_files:
5258 # Find the yapf style config for the current file, defaults to depot
5259 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005260 _FindYapfConfigFile(f, yapf_configs, top_dir)
5261
5262 # Turn on python formatting by default if a yapf config is specified.
5263 # This breaks in the case of this repo though since the specified
5264 # style file is also the global default.
5265 if opts.python is None:
5266 filtered_py_files = []
5267 for f in python_diff_files:
5268 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5269 filtered_py_files.append(f)
5270 else:
5271 filtered_py_files = python_diff_files
5272
5273 # Note: yapf still seems to fix indentation of the entire file
5274 # even if line ranges are specified.
5275 # See https://github.com/google/yapf/issues/499
5276 if not opts.full and filtered_py_files:
5277 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5278
Brian Sheedyb4307d52019-12-02 19:18:17 +00005279 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
5280 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
5281 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00005282
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005283 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005284 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
5285 # Default to pep8 if not .style.yapf is found.
5286 if not yapf_style:
5287 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00005288
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005289 cmd = [yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00005290
5291 has_formattable_lines = False
5292 if not opts.full:
5293 # Only run yapf over changed line ranges.
5294 for diff_start, diff_len in py_line_diffs[f]:
5295 diff_end = diff_start + diff_len - 1
5296 # Yapf errors out if diff_end < diff_start but this
5297 # is a valid line range diff for a removal.
5298 if diff_end >= diff_start:
5299 has_formattable_lines = True
5300 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5301 # If all line diffs were removals we have nothing to format.
5302 if not has_formattable_lines:
5303 continue
5304
5305 if opts.diff or opts.dry_run:
5306 cmd += ['--diff']
5307 # Will return non-zero exit code if non-empty diff.
5308 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5309 if opts.diff:
5310 sys.stdout.write(stdout)
5311 elif len(stdout) > 0:
5312 return_value = 2
5313 else:
5314 cmd += ['-i']
5315 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005316
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005317 # Dart's formatter does not have the nice property of only operating on
5318 # modified chunks, so hard code full.
5319 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005320 try:
5321 command = [dart_format.FindDartFmtToolInChromiumTree()]
5322 if not opts.dry_run and not opts.diff:
5323 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005324 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005325
ppi@chromium.org6593d932016-03-03 15:41:15 +00005326 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005327 if opts.dry_run and stdout:
5328 return_value = 2
Jamie Madill5e96ad12020-01-13 16:08:35 +00005329 except dart_format.NotFoundError:
vapiera7fbd5a2016-06-16 09:17:49 -07005330 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5331 'found in this checkout. Files in other languages are still '
5332 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005333
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005334 # Format GN build files. Always run on full build files for canonical form.
5335 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005336 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005337 if opts.dry_run or opts.diff:
5338 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005339 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005340 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5341 shell=sys.platform == 'win32',
5342 cwd=top_dir)
5343 if opts.dry_run and gn_ret == 2:
5344 return_value = 2 # Not formatted.
5345 elif opts.diff and gn_ret == 2:
5346 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005347 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005348 elif gn_ret != 0:
5349 # For non-dry run cases (and non-2 return values for dry-run), a
5350 # nonzero error code indicates a failure, probably because the file
5351 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005352 DieWithError('gn format failed on ' + gn_diff_file +
5353 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005354
Ilya Shermane081cbe2017-08-15 17:51:04 -07005355 # Skip the metrics formatting from the global presubmit hook. These files have
5356 # a separate presubmit hook that issues an error if the files need formatting,
5357 # whereas the top-level presubmit script merely issues a warning. Formatting
5358 # these files is somewhat slow, so it's important not to duplicate the work.
5359 if not opts.presubmit:
5360 for xml_dir in GetDirtyMetricsDirs(diff_files):
5361 tool_dir = os.path.join(top_dir, xml_dir)
5362 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5363 if opts.dry_run or opts.diff:
5364 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005365 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005366 if opts.diff:
5367 sys.stdout.write(stdout)
5368 if opts.dry_run and stdout:
5369 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005370
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005371 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005372
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005373
Steven Holte2e664bf2017-04-21 13:10:47 -07005374def GetDirtyMetricsDirs(diff_files):
5375 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5376 metrics_xml_dirs = [
5377 os.path.join('tools', 'metrics', 'actions'),
5378 os.path.join('tools', 'metrics', 'histograms'),
5379 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005380 os.path.join('tools', 'metrics', 'ukm'),
5381 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005382 for xml_dir in metrics_xml_dirs:
5383 if any(file.startswith(xml_dir) for file in xml_diff_files):
5384 yield xml_dir
5385
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005386
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005387@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005388@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005389def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005390 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005391 _, args = parser.parse_args(args)
5392
5393 if len(args) != 1:
5394 parser.print_help()
5395 return 1
5396
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005397 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005398 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005399 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005400
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005401 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005402
Edward Lemur52969c92020-02-06 18:15:28 +00005403 issueprefix = Changelist.IssueConfigKey()
5404 output = RunGit(['config', '--local', '--get-regexp',
5405 r'branch\..*\.%s' % issueprefix],
5406 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005407
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005408 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005409 for key, issue in [x.split() for x in output.splitlines()]:
5410 if issue == target_issue:
5411 branches.append(re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key))
5412
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005413 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005414 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005415 return 1
5416 if len(branches) == 1:
5417 RunGit(['checkout', branches[0]])
5418 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005419 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005420 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005421 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005422 which = raw_input('Choose by index: ')
5423 try:
5424 RunGit(['checkout', branches[int(which)]])
5425 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005426 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005427 return 1
5428
5429 return 0
5430
5431
maruel@chromium.org29404b52014-09-08 22:58:00 +00005432def CMDlol(parser, args):
5433 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005434 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005435 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5436 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5437 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005438 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005439 return 0
5440
5441
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005442class OptionParser(optparse.OptionParser):
5443 """Creates the option parse and add --verbose support."""
5444 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005445 optparse.OptionParser.__init__(
5446 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005447 self.add_option(
5448 '-v', '--verbose', action='count', default=0,
5449 help='Use 2 times for more debugging info')
5450
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005451 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005452 try:
5453 return self._parse_args(args)
5454 finally:
5455 # Regardless of success or failure of args parsing, we want to report
5456 # metrics, but only after logging has been initialized (if parsing
5457 # succeeded).
5458 global settings
5459 settings = Settings()
5460
5461 if not metrics.DISABLE_METRICS_COLLECTION:
5462 # GetViewVCUrl ultimately calls logging method.
5463 project_url = settings.GetViewVCUrl().strip('/+')
5464 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5465 metrics.collector.add('project_urls', [project_url])
5466
5467 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005468 # Create an optparse.Values object that will store only the actual passed
5469 # options, without the defaults.
5470 actual_options = optparse.Values()
5471 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5472 # Create an optparse.Values object with the default options.
5473 options = optparse.Values(self.get_default_values().__dict__)
5474 # Update it with the options passed by the user.
5475 options._update_careful(actual_options.__dict__)
5476 # Store the options passed by the user in an _actual_options attribute.
5477 # We store only the keys, and not the values, since the values can contain
5478 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005479 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005480
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005481 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005482 logging.basicConfig(
5483 level=levels[min(options.verbose, len(levels) - 1)],
5484 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5485 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005486
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005487 return options, args
5488
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005489
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005490def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005491 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005492 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005493 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005494 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005495
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005496 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005497 dispatcher = subcommand.CommandDispatcher(__name__)
5498 try:
5499 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005500 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005501 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005502 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005503 if e.code != 500:
5504 raise
5505 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005506 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005507 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005508 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005509
5510
5511if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005512 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5513 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005514 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005515 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005516 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005517 sys.exit(main(sys.argv[1:]))