blob: 2166ea89828471815f72d5e28a17af4a2bf5a1c0 [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
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010014import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000015import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000016import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010017import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010024import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070027import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000029import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000035import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000039import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000041import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000042import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000043import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000044import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000045import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000047import presubmit_canned_checks
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048import presubmit_support
49import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000050import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040051import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000052import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import watchlists
55
Edward Lemur79d4f992019-11-11 23:49:02 +000056from third_party import six
57from six.moves import urllib
58
59
60if sys.version_info.major == 3:
61 basestring = (str,) # pylint: disable=redefined-builtin
62
Edward Lemurb9830242019-10-30 22:19:20 +000063
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
Edward Lemur0f58ae42019-04-30 17:24:12 +000066# Traces for git push will be stored in a traces directory inside the
67# depot_tools checkout.
68DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
69TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000070PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000071
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
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800104POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000105DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000106REFS_THAT_ALIAS_TO_OTHER_REFS = {
107 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
108 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
109}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
thestig@chromium.org44202a22014-03-11 19:22:18 +0000111# Valid extensions for files we want to lint.
112DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
113DEFAULT_LINT_IGNORE_REGEX = r"$^"
114
Aiden Bennerc08566e2018-10-03 17:52:42 +0000115# File name for yapf style config files.
116YAPF_CONFIG_FILENAME = '.style.yapf'
117
Edward Lesmes50da7702020-03-30 19:23:43 +0000118# The issue, patchset and codereview server are stored on git config for each
119# branch under branch.<branch-name>.<config-key>.
120ISSUE_CONFIG_KEY = 'gerritissue'
121PATCHSET_CONFIG_KEY = 'gerritpatchset'
122CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
123
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000124# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000125Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000126
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000127# Initialized in main()
128settings = None
129
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100130# Used by tests/git_cl_test.py to add extra logging.
131# Inside the weirdly failing test, add this:
132# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700133# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100134_IS_BEING_TESTED = False
135
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000136
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000137_KNOWN_GERRIT_TO_SHORT_URLS = {
138 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
139 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
140}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000141assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
142 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000143
144
Christopher Lamf732cd52017-01-24 12:40:11 +1100145def DieWithError(message, change_desc=None):
146 if change_desc:
147 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000148 print('\n ** Content of CL description **\n' +
149 '='*72 + '\n' +
150 change_desc.description + '\n' +
151 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100152
vapiera7fbd5a2016-06-16 09:17:49 -0700153 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000154 sys.exit(1)
155
156
Christopher Lamf732cd52017-01-24 12:40:11 +1100157def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000158 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000159 print('\nsaving CL description to %s\n' % backup_path)
Josip906bfde2020-01-31 22:38:49 +0000160 with open(backup_path, 'w') as backup_file:
161 backup_file.write(change_desc.description)
Christopher Lamf732cd52017-01-24 12:40:11 +1100162
163
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000164def GetNoGitPagerEnv():
165 env = os.environ.copy()
166 # 'cat' is a magical git string that disables pagers on all platforms.
167 env['GIT_PAGER'] = 'cat'
168 return env
169
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000170
bsep@chromium.org627d9002016-04-29 00:00:52 +0000171def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000172 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000173 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
174 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000175 except subprocess2.CalledProcessError as e:
176 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000177 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000178 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000179 'Command "%s" failed.\n%s' % (
180 ' '.join(args), error_message or e.stdout or ''))
Edward Lemur79d4f992019-11-11 23:49:02 +0000181 return e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000182
183
184def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000185 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000186 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000187
188
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000189def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000190 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700191 if suppress_stderr:
192 stderr = subprocess2.VOID
193 else:
194 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000195 try:
tandrii5d48c322016-08-18 16:19:37 -0700196 (out, _), code = subprocess2.communicate(['git'] + args,
197 env=GetNoGitPagerEnv(),
198 stdout=subprocess2.PIPE,
199 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000200 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700201 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900202 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000203 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000204
205
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000206def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000207 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000208 return RunGitWithCode(args, suppress_stderr=True)[1]
209
210
tandrii2a16b952016-10-19 07:09:44 -0700211def time_sleep(seconds):
212 # Use this so that it can be mocked in tests without interfering with python
213 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700214 return time.sleep(seconds)
215
216
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000217def time_time():
218 # Use this so that it can be mocked in tests without interfering with python
219 # system machinery.
220 return time.time()
221
222
Edward Lemur1b52d872019-05-09 21:12:12 +0000223def datetime_now():
224 # Use this so that it can be mocked in tests without interfering with python
225 # system machinery.
226 return datetime.datetime.now()
227
228
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100229def confirm_or_exit(prefix='', action='confirm'):
230 """Asks user to press enter to continue or press Ctrl+C to abort."""
231 if not prefix or prefix.endswith('\n'):
232 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100233 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100234 mid = ' Press'
235 elif prefix.endswith(' '):
236 mid = 'press'
237 else:
238 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000239 gclient_utils.AskForData(
240 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100241
242
243def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000244 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000245 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100246 while True:
247 if 'yes'.startswith(result):
248 return True
249 if 'no'.startswith(result):
250 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000251 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100252
253
machenbach@chromium.org45453142015-09-15 08:45:22 +0000254def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000255 prop_list = getattr(options, 'properties', [])
256 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000257 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000258 try:
259 properties[key] = json.loads(val)
260 except ValueError:
261 pass # If a value couldn't be evaluated, treat it as a string.
262 return properties
263
264
Edward Lemur4c707a22019-09-24 21:13:43 +0000265def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000266 """Calls a buildbucket v2 method and returns the parsed json response."""
267 headers = {
268 'Accept': 'application/json',
269 'Content-Type': 'application/json',
270 }
271 request = json.dumps(request)
272 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
273
274 logging.info('POST %s with %s' % (url, request))
275
276 attempts = 1
277 time_to_sleep = 1
278 while True:
279 response, content = http.request(url, 'POST', body=request, headers=headers)
280 if response.status == 200:
281 return json.loads(content[4:])
282 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
283 msg = '%s error when calling POST %s with %s: %s' % (
284 response.status, url, request, content)
285 raise BuildbucketResponseException(msg)
286 logging.debug(
287 '%s error when calling POST %s with %s. '
288 'Sleeping for %d seconds and retrying...' % (
289 response.status, url, request, time_to_sleep))
290 time.sleep(time_to_sleep)
291 time_to_sleep *= 2
292 attempts += 1
293
294 assert False, 'unreachable'
295
296
Edward Lemur6215c792019-10-03 21:59:05 +0000297def _parse_bucket(raw_bucket):
298 legacy = True
299 project = bucket = None
300 if '/' in raw_bucket:
301 legacy = False
302 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000303 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000304 elif raw_bucket.startswith('luci.'):
305 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000306 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000307 elif '.' in raw_bucket:
308 project = raw_bucket.split('.')[0]
309 bucket = raw_bucket
310 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000311 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000312 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
313 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000314
315
Quinten Yearsley777660f2020-03-04 23:37:06 +0000316def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000317 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700318
319 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000320 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000321 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700322 options: Command-line options.
323 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000324 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000325 for project, bucket, builder in jobs:
326 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000327 print('To see results here, run: git cl try-results')
328 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700329
Quinten Yearsley777660f2020-03-04 23:37:06 +0000330 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000331 if not requests:
332 return
333
Edward Lemur5b929a42019-10-21 17:57:39 +0000334 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000335 http.force_exception_to_status_code = True
336
337 batch_request = {'requests': requests}
338 batch_response = _call_buildbucket(
339 http, options.buildbucket_host, 'Batch', batch_request)
340
341 errors = [
342 ' ' + response['error']['message']
343 for response in batch_response.get('responses', [])
344 if 'error' in response
345 ]
346 if errors:
347 raise BuildbucketResponseException(
348 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
349
350
Quinten Yearsley777660f2020-03-04 23:37:06 +0000351def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000352 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000353 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000354 shared_properties = {
355 'category': options.ensure_value('category', 'git_cl_try')
356 }
357 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000358 shared_properties['clobber'] = True
359 shared_properties.update(_get_properties_from_options(options) or {})
360
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000361 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000362 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000363 shared_tags.append({'key': 'retry_failed',
364 'value': '1'})
365
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000366 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000367 for (project, bucket, builder) in jobs:
368 properties = shared_properties.copy()
369 if 'presubmit' in builder.lower():
370 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000371
Edward Lemur45768512020-03-02 19:03:14 +0000372 requests.append({
373 'scheduleBuild': {
374 'requestId': str(uuid.uuid4()),
375 'builder': {
376 'project': getattr(options, 'project', None) or project,
377 'bucket': bucket,
378 'builder': builder,
379 },
380 'gerritChanges': gerrit_changes,
381 'properties': properties,
382 'tags': [
383 {'key': 'builder', 'value': builder},
384 ] + shared_tags,
385 }
386 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000387
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000388 if options.ensure_value('revision', None):
Edward Lemur45768512020-03-02 19:03:14 +0000389 requests[-1]['scheduleBuild']['gitilesCommit'] = {
390 'host': gerrit_changes[0]['host'],
391 'project': gerrit_changes[0]['project'],
392 'id': options.revision
393 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000394
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000395 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
Quinten Yearsley777660f2020-03-04 23:37:06 +0000398def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000399 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000401 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000402 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000403 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000404 request = {
405 'predicate': {
406 'gerritChanges': [changelist.GetGerritChange(patchset)],
407 },
408 'fields': ','.join('builds.*.' + field for field in fields),
409 }
tandrii221ab252016-10-06 08:12:04 -0700410
Edward Lemur5b929a42019-10-21 17:57:39 +0000411 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000412 if authenticator.has_cached_credentials():
413 http = authenticator.authorize(httplib2.Http())
414 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700415 print('Warning: Some results might be missing because %s' %
416 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000417 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000418 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 http.force_exception_to_status_code = True
420
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000421 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
422 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000423
Edward Lemur45768512020-03-02 19:03:14 +0000424
Edward Lemur5b929a42019-10-21 17:57:39 +0000425def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000426 """Fetches builds from the latest patchset that has builds (within
427 the last few patchsets).
428
429 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000430 changelist (Changelist): The CL to fetch builds for
431 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000432 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
433 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000434 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000435 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
436 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000437 """
438 assert buildbucket_host
439 assert changelist.GetIssue(), 'CL must be uploaded first'
440 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000441 if latest_patchset is None:
442 assert changelist.GetMostRecentPatchset()
443 ps = changelist.GetMostRecentPatchset()
444 else:
445 assert latest_patchset > 0, latest_patchset
446 ps = latest_patchset
447
Quinten Yearsley983111f2019-09-26 17:18:48 +0000448 min_ps = max(1, ps - 5)
449 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000450 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000451 if len(builds):
452 return builds, ps
453 ps -= 1
454 return [], 0
455
456
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000457def _filter_failed_for_retry(all_builds):
458 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000459
460 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000461 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000462 i.e. a list of buildbucket.v2.Builds which includes status and builder
463 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000464
465 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000466 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000467 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000468 """
Edward Lemur45768512020-03-02 19:03:14 +0000469 grouped = {}
470 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000471 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000472 key = (builder['project'], builder['bucket'], builder['builder'])
473 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000474
Edward Lemur45768512020-03-02 19:03:14 +0000475 jobs = []
476 for (project, bucket, builder), builds in grouped.items():
477 if 'triggered' in builder:
478 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
479 'from a parent. Please schedule a manual job for the parent '
480 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000481 continue
482 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
483 # Don't retry if any are running.
484 continue
Edward Lemur45768512020-03-02 19:03:14 +0000485 # If builder had several builds, retry only if the last one failed.
486 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
487 # build, but in case of retrying failed jobs retrying a flaky one makes
488 # sense.
489 builds = sorted(builds, key=lambda b: b['createTime'])
490 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
491 continue
492 # Don't retry experimental build previously triggered by CQ.
493 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
494 for t in builds[-1]['tags']):
495 continue
496 jobs.append((project, bucket, builder))
497
498 # Sort the jobs to make testing easier.
499 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000500
501
Quinten Yearsley777660f2020-03-04 23:37:06 +0000502def _print_tryjobs(options, builds):
503 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000505 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000506 return
507
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000508 longest_builder = max(len(b['builder']['builder']) for b in builds)
509 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000511 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
512 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000514 builds_by_status = {}
515 for b in builds:
516 builds_by_status.setdefault(b['status'], []).append({
517 'id': b['id'],
518 'name': name_fmt.format(
519 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
520 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000522 sort_key = lambda b: (b['name'], b['id'])
523
524 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000526 if not builds:
527 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000529 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000530 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000531 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532 else:
533 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
534
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000535 print(colorize(title))
536 for b in sorted(builds, key=sort_key):
537 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538
539 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000540 print_builds(
541 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
542 print_builds(
543 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
544 color=Fore.MAGENTA)
545 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
546 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
547 color=Fore.MAGENTA)
548 print_builds('Started:', builds_by_status.pop('STARTED', []))
549 print_builds(
550 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000552 print_builds(
553 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000554 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555
556
Aiden Bennerc08566e2018-10-03 17:52:42 +0000557def _ComputeDiffLineRanges(files, upstream_commit):
558 """Gets the changed line ranges for each file since upstream_commit.
559
560 Parses a git diff on provided files and returns a dict that maps a file name
561 to an ordered list of range tuples in the form (start_line, count).
562 Ranges are in the same format as a git diff.
563 """
564 # If files is empty then diff_output will be a full diff.
565 if len(files) == 0:
566 return {}
567
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000568 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000569 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000570 diff_output = RunGit(diff_cmd)
571
572 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
573 # 2 capture groups
574 # 0 == fname of diff file
575 # 1 == 'diff_start,diff_count' or 'diff_start'
576 # will match each of
577 # diff --git a/foo.foo b/foo.py
578 # @@ -12,2 +14,3 @@
579 # @@ -12,2 +17 @@
580 # running re.findall on the above string with pattern will give
581 # [('foo.py', ''), ('', '14,3'), ('', '17')]
582
583 curr_file = None
584 line_diffs = {}
585 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
586 if match[0] != '':
587 # Will match the second filename in diff --git a/a.py b/b.py.
588 curr_file = match[0]
589 line_diffs[curr_file] = []
590 else:
591 # Matches +14,3
592 if ',' in match[1]:
593 diff_start, diff_count = match[1].split(',')
594 else:
595 # Single line changes are of the form +12 instead of +12,1.
596 diff_start = match[1]
597 diff_count = 1
598
599 diff_start = int(diff_start)
600 diff_count = int(diff_count)
601
602 # If diff_count == 0 this is a removal we can ignore.
603 line_diffs[curr_file].append((diff_start, diff_count))
604
605 return line_diffs
606
607
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000608def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000609 """Checks if a yapf file is in any parent directory of fpath until top_dir.
610
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000611 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000612 is found returns None. Uses yapf_config_cache as a cache for previously found
613 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000614 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000615 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000616 # Return result if we've already computed it.
617 if fpath in yapf_config_cache:
618 return yapf_config_cache[fpath]
619
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000620 parent_dir = os.path.dirname(fpath)
621 if os.path.isfile(fpath):
622 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000623 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000624 # Otherwise fpath is a directory
625 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
626 if os.path.isfile(yapf_file):
627 ret = yapf_file
628 elif fpath == top_dir or parent_dir == fpath:
629 # If we're at the top level directory, or if we're at root
630 # there is no provided style.
631 ret = None
632 else:
633 # Otherwise recurse on the current directory.
634 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000635 yapf_config_cache[fpath] = ret
636 return ret
637
638
Brian Sheedyb4307d52019-12-02 19:18:17 +0000639def _GetYapfIgnorePatterns(top_dir):
640 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000641
642 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
643 but this functionality appears to break when explicitly passing files to
644 yapf for formatting. According to
645 https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
646 the .yapfignore file should be in the directory that yapf is invoked from,
647 which we assume to be the top level directory in this case.
648
649 Args:
650 top_dir: The top level directory for the repository being formatted.
651
652 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000653 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000654 """
655 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000656 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000657 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000658 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000659
Brian Sheedyb4307d52019-12-02 19:18:17 +0000660 with open(yapfignore_file) as f:
661 for line in f.readlines():
662 stripped_line = line.strip()
663 # Comments and blank lines should be ignored.
664 if stripped_line.startswith('#') or stripped_line == '':
665 continue
666 ignore_patterns.add(stripped_line)
667 return ignore_patterns
668
669
670def _FilterYapfIgnoredFiles(filepaths, patterns):
671 """Filters out any filepaths that match any of the given patterns.
672
673 Args:
674 filepaths: An iterable of strings containing filepaths to filter.
675 patterns: An iterable of strings containing fnmatch patterns to filter on.
676
677 Returns:
678 A list of strings containing all the elements of |filepaths| that did not
679 match any of the patterns in |patterns|.
680 """
681 # Not inlined so that tests can use the same implementation.
682 return [f for f in filepaths
683 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000684
685
Aaron Gable13101a62018-02-09 13:20:41 -0800686def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000687 """Prints statistics about the change to the user."""
688 # --no-ext-diff is broken in some versions of Git, so try to work around
689 # this by overriding the environment (but there is still a problem if the
690 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000691 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000692 if 'GIT_EXTERNAL_DIFF' in env:
693 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000694
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000695 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800696 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000697 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000698
699
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000700class BuildbucketResponseException(Exception):
701 pass
702
703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000704class Settings(object):
705 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000707 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 self.tree_status_url = None
709 self.viewvc_url = None
710 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000711 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000712 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000713 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000714 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000715 self.format_full_by_default = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716
Edward Lemur26964072020-02-19 19:18:51 +0000717 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000719 if self.updated:
720 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000721
Edward Lemur26964072020-02-19 19:18:51 +0000722 # The only value that actually changes the behavior is
723 # autoupdate = "false". Everything else means "true".
724 autoupdate = (
725 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
726
727 cr_settings_file = FindCodereviewSettingsFile()
728 if autoupdate != 'false' and cr_settings_file:
729 LoadCodereviewSettingsFromFile(cr_settings_file)
730 cr_settings_file.close()
731
732 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000734 @staticmethod
735 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000736 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000739 if self.root is None:
740 self.root = os.path.abspath(self.GetRelativeRoot())
741 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 def GetTreeStatusUrl(self, error_ok=False):
744 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000745 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
746 if self.tree_status_url is None and not error_ok:
747 DieWithError(
748 'You must configure your tree status URL by running '
749 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750 return self.tree_status_url
751
752 def GetViewVCUrl(self):
753 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000754 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 return self.viewvc_url
756
rmistry@google.com90752582014-01-14 21:04:50 +0000757 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000758 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000759
rmistry@google.com5626a922015-02-26 14:03:30 +0000760 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000761 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000762 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000763 return run_post_upload_hook == "True"
764
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000765 def GetDefaultCCList(self):
Edward Lemur26964072020-02-19 19:18:51 +0000766 return self._GetConfig('rietveld.cc')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000767
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000768 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000769 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000771 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
772 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000773 # Default is squash now (http://crbug.com/611892#c23).
774 self.squash_gerrit_uploads = self._GetConfig(
775 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000776 return self.squash_gerrit_uploads
777
Edward Lesmes4de54132020-05-05 19:41:33 +0000778 def GetSquashGerritUploadsOverride(self):
779 """Return True or False if codereview.settings should be overridden.
780
781 Returns None if no override has been defined.
782 """
783 # See also http://crbug.com/611892#c23
784 result = self._GetConfig('gerrit.override-squash-uploads').lower()
785 if result == 'true':
786 return True
787 if result == 'false':
788 return False
789 return None
790
tandrii@chromium.org28253532016-04-14 13:46:56 +0000791 def GetGerritSkipEnsureAuthenticated(self):
792 """Return True if EnsureAuthenticated should not be done for Gerrit
793 uploads."""
794 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000795 self.gerrit_skip_ensure_authenticated = self._GetConfig(
796 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000797 return self.gerrit_skip_ensure_authenticated
798
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000799 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000800 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000801 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000802 # Git requires single quotes for paths with spaces. We need to replace
803 # them with double quotes for Windows to treat such paths as a single
804 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000805 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000806 return self.git_editor or None
807
thestig@chromium.org44202a22014-03-11 19:22:18 +0000808 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000809 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000810
811 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000812 return self._GetConfig(
813 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000814
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000815 def GetFormatFullByDefault(self):
816 if self.format_full_by_default is None:
817 result = (
818 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
819 error_ok=True).strip())
820 self.format_full_by_default = (result == 'true')
821 return self.format_full_by_default
822
Edward Lemur26964072020-02-19 19:18:51 +0000823 def _GetConfig(self, key, default=''):
824 self._LazyUpdateIfNeeded()
825 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
827
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000828class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000829 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000830 NONE = 'none'
831 DRY_RUN = 'dry_run'
832 COMMIT = 'commit'
833
834 ALL_STATES = [NONE, DRY_RUN, COMMIT]
835
836
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000837class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000838 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000839 self.issue = issue
840 self.patchset = patchset
841 self.hostname = hostname
842
843 @property
844 def valid(self):
845 return self.issue is not None
846
847
Edward Lemurf38bc172019-09-03 21:02:13 +0000848def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000849 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
850 fail_result = _ParsedIssueNumberArgument()
851
Edward Lemur678a6842019-10-03 22:25:05 +0000852 if isinstance(arg, int):
853 return _ParsedIssueNumberArgument(issue=arg)
854 if not isinstance(arg, basestring):
855 return fail_result
856
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000857 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000858 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000859 if not arg.startswith('http'):
860 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700861
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000862 url = gclient_utils.UpgradeToHttps(arg)
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000863 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
864 if url.startswith(short_url):
865 url = gerrit_url + url[len(short_url):]
866 break
867
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000868 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000869 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000870 except ValueError:
871 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200872
Edward Lemur678a6842019-10-03 22:25:05 +0000873 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
874 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
875 # Short urls like https://domain/<issue_number> can be used, but don't allow
876 # specifying the patchset (you'd 404), but we allow that here.
877 if parsed_url.path == '/':
878 part = parsed_url.fragment
879 else:
880 part = parsed_url.path
881
882 match = re.match(
883 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
884 if not match:
885 return fail_result
886
887 issue = int(match.group('issue'))
888 patchset = match.group('patchset')
889 return _ParsedIssueNumberArgument(
890 issue=issue,
891 patchset=int(patchset) if patchset else None,
892 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000893
894
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000895def _create_description_from_log(args):
896 """Pulls out the commit log to use as a base for the CL description."""
897 log_args = []
898 if len(args) == 1 and not args[0].endswith('.'):
899 log_args = [args[0] + '..']
900 elif len(args) == 1 and args[0].endswith('...'):
901 log_args = [args[0][:-1]]
902 elif len(args) == 2:
903 log_args = [args[0] + '..' + args[1]]
904 else:
905 log_args = args[:] # Hope for the best!
Manh Nguyen77463bb2020-06-11 17:26:12 +0000906 return RunGit(['log', '--pretty=format:%B'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000907
908
Aaron Gablea45ee112016-11-22 15:14:38 -0800909class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700910 def __init__(self, issue, url):
911 self.issue = issue
912 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800913 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700914
915 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800916 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700917 self.issue, self.url)
918
919
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100920_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +0000921 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100922 # TODO(tandrii): these two aren't known in Gerrit.
923 'approval', 'disapproval'])
924
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000927 """Changelist works with one changelist in local branch.
928
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000929 Notes:
930 * Not safe for concurrent multi-{thread,process} use.
931 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700932 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 """
934
Edward Lemur125d60a2019-09-13 18:25:41 +0000935 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000936 """Create a new ChangeList instance.
937
Edward Lemurf38bc172019-09-03 21:02:13 +0000938 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000939 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000941 global settings
942 if not settings:
943 # Happens when git_cl.py is used as a utility library.
944 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.branchref = branchref
947 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000948 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +0000949 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 else:
951 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000953 self.lookedup_issue = False
954 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000958 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -0800959 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000960 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +0000961 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000962
Edward Lemur125d60a2019-09-13 18:25:41 +0000963 # Lazily cached values.
964 self._gerrit_host = None # e.g. chromium-review.googlesource.com
965 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
966 # Map from change number (issue) to its detail cache.
967 self._detail_cache = {}
968
969 if codereview_host is not None:
970 assert not codereview_host.startswith('https://'), codereview_host
971 self._gerrit_host = codereview_host
972 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000973
974 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700975 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700977 The return value is a string suitable for passing to git cl with the --cc
978 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000979 """
980 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000981 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -0800982 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000983 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
984 return self.cc
985
Daniel Cheng7227d212017-11-17 08:12:37 -0800986 def ExtendCC(self, more_cc):
987 """Extends the list of users to cc on this CL based on the changed files."""
988 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990 def GetBranch(self):
991 """Returns the short branch name, e.g. 'master'."""
992 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +0000993 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000994 if not branchref:
995 return None
996 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +0000997 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 return self.branch
999
1000 def GetBranchRef(self):
1001 """Returns the full branch name, e.g. 'refs/heads/master'."""
1002 self.GetBranch() # Poke the lazy loader.
1003 return self.branchref
1004
Edward Lemur85153282020-02-14 22:06:29 +00001005 def _GitGetBranchConfigValue(self, key, default=None):
1006 return scm.GIT.GetBranchConfig(
1007 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001008
Edward Lemur85153282020-02-14 22:06:29 +00001009 def _GitSetBranchConfigValue(self, key, value):
1010 action = 'set %s to %r' % (key, value)
1011 if not value:
1012 action = 'unset %s' % key
1013 assert self.GetBranch(), 'a branch is needed to ' + action
1014 return scm.GIT.SetBranchConfig(
1015 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001016
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001017 @staticmethod
1018 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001019 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 e.g. 'origin', 'refs/heads/master'
1021 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001022 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1023 settings.GetRoot(), branch)
1024 if not remote or not upstream_branch:
1025 DieWithError(
1026 'Unable to determine default branch to diff against.\n'
1027 'Either pass complete "git diff"-style arguments, like\n'
1028 ' git cl upload origin/master\n'
1029 'or verify this branch is set up to track another \n'
1030 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
1032 return remote, upstream_branch
1033
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001034 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001035 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001036 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001037 DieWithError('The upstream for the current branch (%s) does not exist '
1038 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001039 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001040 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 def GetUpstreamBranch(self):
1043 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001044 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001045 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001046 upstream_branch = upstream_branch.replace('refs/heads/',
1047 'refs/remotes/%s/' % remote)
1048 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1049 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 self.upstream_branch = upstream_branch
1051 return self.upstream_branch
1052
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001053 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001054 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 remote, branch = None, self.GetBranch()
1056 seen_branches = set()
1057 while branch not in seen_branches:
1058 seen_branches.add(branch)
1059 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001060 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 if remote != '.' or branch.startswith('refs/remotes'):
1062 break
1063 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001064 remotes = RunGit(['remote'], error_ok=True).split()
1065 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001066 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001067 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001069 logging.warn('Could not determine which remote this change is '
1070 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001071 else:
1072 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001073 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 branch = 'HEAD'
1075 if branch.startswith('refs/remotes'):
1076 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001077 elif branch.startswith('refs/branch-heads/'):
1078 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001079 else:
1080 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001081 return self._remote
1082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 def GetRemoteUrl(self):
1084 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1085
1086 Returns None if there is no remote.
1087 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001088 is_cached, value = self._cached_remote_url
1089 if is_cached:
1090 return value
1091
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001092 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001093 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001094
Edward Lemur298f2cf2019-02-22 21:40:39 +00001095 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001096 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001097 if host:
1098 self._cached_remote_url = (True, url)
1099 return url
1100
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001101 # If it cannot be parsed as an url, assume it is a local directory,
1102 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001103 logging.warning('"%s" doesn\'t appear to point to a git host. '
1104 'Interpreting it as a local directory.', url)
1105 if not os.path.isdir(url):
1106 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001107 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1108 'but it doesn\'t exist.',
1109 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001110 return None
1111
1112 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001113 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001114
Edward Lemur79d4f992019-11-11 23:49:02 +00001115 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001116 if not host:
1117 logging.error(
1118 'Remote "%(remote)s" for branch "%(branch)s" points to '
1119 '"%(cache_path)s", but it is misconfigured.\n'
1120 '"%(cache_path)s" must be a git repo and must have a remote named '
1121 '"%(remote)s" pointing to the git host.', {
1122 'remote': remote,
1123 'cache_path': cache_path,
1124 'branch': self.GetBranch()})
1125 return None
1126
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001127 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001128 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001130 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001131 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001132 if self.issue is None and not self.lookedup_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00001133 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001134 if self.issue is not None:
1135 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 return self.issue
1138
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001139 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 issue = self.GetIssue()
1142 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001143 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001144 server = self.GetCodereviewServer()
1145 if short:
1146 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1147 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
Edward Lemur6c6827c2020-02-06 21:15:18 +00001149 def FetchDescription(self, pretty=False):
1150 assert self.GetIssue(), 'issue is required to query Gerrit'
1151
Edward Lemur9aa1a962020-02-25 00:58:38 +00001152 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001153 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1154 current_rev = data['current_revision']
1155 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001156
1157 if not pretty:
1158 return self.description
1159
1160 # Set width to 72 columns + 2 space indent.
1161 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1162 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1163 lines = self.description.splitlines()
1164 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
1166 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001167 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001168 if self.patchset is None and not self.lookedup_patchset:
Edward Lesmes50da7702020-03-30 19:23:43 +00001169 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001170 if self.patchset is not None:
1171 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 return self.patchset
1174
Edward Lemur9aa1a962020-02-25 00:58:38 +00001175 def GetAuthor(self):
1176 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001179 """Set this branch's patchset. If patchset=0, clears the patchset."""
1180 assert self.GetBranch()
1181 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001182 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001183 else:
1184 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001185 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001187 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001188 """Set this branch's issue. If issue isn't given, clears the issue."""
1189 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001191 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001192 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001193 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001194 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001196 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001197 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 else:
tandrii5d48c322016-08-18 16:19:37 -07001199 # Reset all of these just to be clean.
1200 reset_suffixes = [
1201 'last-upload-hash',
Edward Lesmes50da7702020-03-30 19:23:43 +00001202 ISSUE_CONFIG_KEY,
1203 PATCHSET_CONFIG_KEY,
1204 CODEREVIEW_SERVER_CONFIG_KEY,
1205 'gerritsquashhash',
1206 ]
tandrii5d48c322016-08-18 16:19:37 -07001207 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001208 try:
1209 self._GitSetBranchConfigValue(prop, None)
1210 except subprocess2.CalledProcessError:
1211 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001212 msg = RunGit(['log', '-1', '--format=%B']).strip()
1213 if msg and git_footers.get_footer_change_id(msg):
1214 print('WARNING: The change patched into this branch has a Change-Id. '
1215 'Removing it.')
1216 RunGit(['commit', '--amend', '-m',
1217 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001218 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001219 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001220 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221
Edward Lemur2c62b332020-03-12 22:12:33 +00001222 def GetAffectedFiles(self, upstream):
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001223 try:
Edward Lemur2c62b332020-03-12 22:12:33 +00001224 return [f for _, f in scm.GIT.CaptureStatus(settings.GetRoot(), upstream)]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001225 except subprocess2.CalledProcessError:
1226 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001227 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001228 'This branch probably doesn\'t exist anymore. To reset the\n'
1229 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001230 ' git branch --set-upstream-to origin/master %s\n'
1231 'or replace origin/master with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001232 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001233
dsansomee2d6fd92016-09-08 00:10:47 -07001234 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001235 assert self.GetIssue(), 'issue is required to update description'
1236
1237 if gerrit_util.HasPendingChangeEdit(
1238 self._GetGerritHost(), self._GerritChangeIdentifier()):
1239 if not force:
1240 confirm_or_exit(
1241 'The description cannot be modified while the issue has a pending '
1242 'unpublished edit. Either publish the edit in the Gerrit web UI '
1243 'or delete it.\n\n', action='delete the unpublished edit')
1244
1245 gerrit_util.DeletePendingChangeEdit(
1246 self._GetGerritHost(), self._GerritChangeIdentifier())
1247 gerrit_util.SetCommitMessage(
1248 self._GetGerritHost(), self._GerritChangeIdentifier(),
1249 description, notify='NONE')
1250
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001251 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001252
Edward Lemur75526302020-02-27 22:31:05 +00001253 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001254 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001255 '--root', settings.GetRoot(),
1256 '--upstream', upstream,
1257 ]
1258
1259 args.extend(['--verbose'] * verbose)
1260
Edward Lemur99df04e2020-03-05 19:39:43 +00001261 author = self.GetAuthor()
1262 gerrit_url = self.GetCodereviewServer()
Edward Lemur227d5102020-02-25 23:45:35 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001265 if author:
1266 args.extend(['--author', author])
1267 if gerrit_url:
1268 args.extend(['--gerrit_url', gerrit_url])
Edward Lemur227d5102020-02-25 23:45:35 +00001269 if issue:
1270 args.extend(['--issue', str(issue)])
1271 if patchset:
1272 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001273
Edward Lemur75526302020-02-27 22:31:05 +00001274 return args
1275
1276 def RunHook(
1277 self, committing, may_prompt, verbose, parallel, upstream, description,
1278 all_files):
1279 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1280 args = self._GetCommonPresubmitArgs(verbose, upstream)
1281 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001282 if may_prompt:
1283 args.append('--may_prompt')
1284 if parallel:
1285 args.append('--parallel')
1286 if all_files:
1287 args.append('--all_files')
1288
1289 with gclient_utils.temporary_file() as description_file:
1290 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001291
1292 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001293 args.extend(['--json_output', json_output])
1294 args.extend(['--description_file', description_file])
1295
1296 start = time_time()
1297 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1298 exit_code = p.wait()
1299 metrics.collector.add_repeated('sub_commands', {
1300 'command': 'presubmit',
1301 'execution_time': time_time() - start,
1302 'exit_code': exit_code,
1303 })
1304
1305 if exit_code:
1306 sys.exit(exit_code)
1307
1308 json_results = gclient_utils.FileRead(json_output)
1309 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310
Edward Lemur75526302020-02-27 22:31:05 +00001311 def RunPostUploadHook(self, verbose, upstream, description):
1312 args = self._GetCommonPresubmitArgs(verbose, upstream)
1313 args.append('--post_upload')
1314
1315 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001316 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001317 args.extend(['--description_file', description_file])
1318 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1319 p.wait()
1320
Edward Lemur5a644f82020-03-18 16:44:57 +00001321 def _GetDescriptionForUpload(self, options, git_diff_args, files):
1322 # Get description message for upload.
1323 if self.GetIssue():
1324 description = self.FetchDescription()
1325 elif options.message:
1326 description = options.message
1327 else:
1328 description = _create_description_from_log(git_diff_args)
1329 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001330 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001331
1332 # Extract bug number from branch name.
1333 bug = options.bug
1334 fixed = options.fixed
1335 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
1336 self.GetBranch())
1337 if not bug and not fixed and match:
1338 if match.group('type') == 'bug':
1339 bug = match.group('bugnum')
1340 else:
1341 fixed = match.group('bugnum')
1342
1343 change_description = ChangeDescription(description, bug, fixed)
1344
1345 # Set the reviewer list now so that presubmit checks can access it.
1346 if options.reviewers or options.tbrs or options.add_owners_to:
1347 change_description.update_reviewers(
1348 options.reviewers, options.tbrs, options.add_owners_to, files,
1349 self.GetAuthor())
1350
1351 return change_description
1352
1353 def _GetTitleForUpload(self, options):
1354 # When not squashing, just return options.title.
1355 if not options.squash:
1356 return options.title
1357
1358 # On first upload, patchset title is always this string, while options.title
1359 # gets converted to first line of message.
1360 if not self.GetIssue():
1361 return 'Initial upload'
1362
1363 # When uploading subsequent patchsets, options.message is taken as the title
1364 # if options.title is not provided.
1365 if options.title:
1366 return options.title
1367 if options.message:
1368 return options.message.strip()
1369
1370 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001371 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001372 if options.force:
1373 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001374 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
1375 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001376
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001377 def CMDUpload(self, options, git_diff_args, orig_args):
1378 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001379 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001380 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001381 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001382 else:
1383 if self.GetBranch() is None:
1384 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1385
1386 # Default to diffing against common ancestor of upstream branch
1387 base_branch = self.GetCommonAncestorWithUpstream()
1388 git_diff_args = [base_branch, 'HEAD']
1389
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001390 # Fast best-effort checks to abort before running potentially expensive
1391 # hooks if uploading is likely to fail anyway. Passing these checks does
1392 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001393 self.EnsureAuthenticated(force=options.force)
1394 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001395
1396 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001397 watchlist = watchlists.Watchlists(settings.GetRoot())
1398 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001399 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001400 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001401
Edward Lemur5a644f82020-03-18 16:44:57 +00001402 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001403 if not options.bypass_hooks:
Edward Lemur2c62b332020-03-12 22:12:33 +00001404 hook_results = self.RunHook(
1405 committing=False,
1406 may_prompt=not options.force,
1407 verbose=options.verbose,
1408 parallel=options.parallel,
1409 upstream=base_branch,
Edward Lemur5a644f82020-03-18 16:44:57 +00001410 description=change_desc.description,
Edward Lemur2c62b332020-03-12 22:12:33 +00001411 all_files=False)
Edward Lemur227d5102020-02-25 23:45:35 +00001412 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001413
Aaron Gable13101a62018-02-09 13:20:41 -08001414 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001415 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001416 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001417 if not ret:
Edward Lemur85153282020-02-14 22:06:29 +00001418 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001419 'last-upload-hash', scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001420 # Run post upload hooks, if specified.
1421 if settings.GetRunPostUploadHook():
Edward Lemur5a644f82020-03-18 16:44:57 +00001422 self.RunPostUploadHook(
1423 options.verbose, base_branch, change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001424
1425 # Upload all dependencies if specified.
1426 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001427 print()
1428 print('--dependencies has been specified.')
1429 print('All dependent local branches will be re-uploaded.')
1430 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001431 # Remove the dependencies flag from args so that we do not end up in a
1432 # loop.
1433 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001434 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001435 return ret
1436
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001437 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001438 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001439
1440 Issue must have been already uploaded and known.
1441 """
1442 assert new_state in _CQState.ALL_STATES
1443 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001444 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001445 vote_map = {
1446 _CQState.NONE: 0,
1447 _CQState.DRY_RUN: 1,
1448 _CQState.COMMIT: 2,
1449 }
1450 labels = {'Commit-Queue': vote_map[new_state]}
1451 notify = False if new_state == _CQState.DRY_RUN else None
1452 gerrit_util.SetReview(
1453 self._GetGerritHost(), self._GerritChangeIdentifier(),
1454 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001455 return 0
1456 except KeyboardInterrupt:
1457 raise
1458 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001459 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001460 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001461 ' * Your project has no CQ,\n'
1462 ' * You don\'t have permission to change the CQ state,\n'
1463 ' * There\'s a bug in this code (see stack trace below).\n'
1464 'Consider specifying which bots to trigger manually or asking your '
1465 'project owners for permissions or contacting Chrome Infra at:\n'
1466 'https://www.chromium.org/infra\n\n' %
1467 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001468 # Still raise exception so that stack trace is printed.
1469 raise
1470
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001471 def _GetGerritHost(self):
1472 # Lazy load of configs.
1473 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001474 if self._gerrit_host and '.' not in self._gerrit_host:
1475 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1476 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001477 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001478 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001479 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001480 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001481 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1482 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001483 return self._gerrit_host
1484
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001485 def _GetGitHost(self):
1486 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001487 remote_url = self.GetRemoteUrl()
1488 if not remote_url:
1489 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001490 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001491
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492 def GetCodereviewServer(self):
1493 if not self._gerrit_server:
1494 # If we're on a branch then get the server potentially associated
1495 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001496 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001497 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001498 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001499 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001500 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001501 if not self._gerrit_server:
1502 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1503 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001504 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001505 parts[0] = parts[0] + '-review'
1506 self._gerrit_host = '.'.join(parts)
1507 self._gerrit_server = 'https://%s' % self._gerrit_host
1508 return self._gerrit_server
1509
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001510 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001511 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001512 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001513 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001514 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001515 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001516 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001517 if project.endswith('.git'):
1518 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001519 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1520 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1521 # gitiles/git-over-https protocol. E.g.,
1522 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1523 # as
1524 # https://chromium.googlesource.com/v8/v8
1525 if project.startswith('a/'):
1526 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001527 return project
1528
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001529 def _GerritChangeIdentifier(self):
1530 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1531
1532 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001533 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001534 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001535 project = self._GetGerritProject()
1536 if project:
1537 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1538 # Fall back on still unique, but less efficient change number.
1539 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001540
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001541 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001542 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001543 if settings.GetGerritSkipEnsureAuthenticated():
1544 # For projects with unusual authentication schemes.
1545 # See http://crbug.com/603378.
1546 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001547
1548 # Check presence of cookies only if using cookies-based auth method.
1549 cookie_auth = gerrit_util.Authenticator.get()
1550 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001551 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001552
Florian Mayerae510e82020-01-30 21:04:48 +00001553 remote_url = self.GetRemoteUrl()
1554 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001555 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001556 return
1557 if urllib.parse.urlparse(remote_url).scheme != 'https':
Josip906bfde2020-01-31 22:38:49 +00001558 logging.warning('Ignoring branch %(branch)s with non-https remote '
1559 '%(remote)s', {
1560 'branch': self.branch,
1561 'remote': self.GetRemoteUrl()
1562 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001563 return
1564
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001565 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001566 self.GetCodereviewServer()
1567 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001568 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001569
1570 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1571 git_auth = cookie_auth.get_auth_header(git_host)
1572 if gerrit_auth and git_auth:
1573 if gerrit_auth == git_auth:
1574 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001575 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001576 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001577 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001578 ' %s\n'
1579 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001580 ' Consider running the following command:\n'
1581 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001582 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001583 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001584 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001585 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001586 cookie_auth.get_new_password_message(git_host)))
1587 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001588 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001589 return
1590 else:
1591 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001592 ([] if gerrit_auth else [self._gerrit_host]) +
1593 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001594 DieWithError('Credentials for the following hosts are required:\n'
1595 ' %s\n'
1596 'These are read from %s (or legacy %s)\n'
1597 '%s' % (
1598 '\n '.join(missing),
1599 cookie_auth.get_gitcookies_path(),
1600 cookie_auth.get_netrc_path(),
1601 cookie_auth.get_new_password_message(git_host)))
1602
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001603 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001604 if not self.GetIssue():
1605 return
1606
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001607 status = self._GetChangeDetail()['status']
1608 if status in ('MERGED', 'ABANDONED'):
1609 DieWithError('Change %s has been %s, new uploads are not allowed' %
1610 (self.GetIssueURL(),
1611 'submitted' if status == 'MERGED' else 'abandoned'))
1612
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001613 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1614 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1615 # Apparently this check is not very important? Otherwise get_auth_email
1616 # could have been added to other implementations of Authenticator.
1617 cookies_auth = gerrit_util.Authenticator.get()
1618 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001619 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001620
1621 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001622 if self.GetIssueOwner() == cookies_user:
1623 return
1624 logging.debug('change %s owner is %s, cookies user is %s',
1625 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001626 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001627 # so ask what Gerrit thinks of this user.
1628 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1629 if details['email'] == self.GetIssueOwner():
1630 return
1631 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001632 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001633 'as %s.\n'
1634 'Uploading may fail due to lack of permissions.' %
1635 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1636 confirm_or_exit(action='upload')
1637
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001638 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001639 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001640 or CQ status, assuming adherence to a common workflow.
1641
1642 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001643 * 'error' - error from review tool (including deleted issues)
1644 * 'unsent' - no reviewers added
1645 * 'waiting' - waiting for review
1646 * 'reply' - waiting for uploader to reply to review
1647 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001648 * 'dry-run' - dry-running in the CQ
1649 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001650 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001651 """
1652 if not self.GetIssue():
1653 return None
1654
1655 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001656 data = self._GetChangeDetail([
1657 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001658 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001659 return 'error'
1660
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001661 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001662 return 'closed'
1663
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001664 cq_label = data['labels'].get('Commit-Queue', {})
1665 max_cq_vote = 0
1666 for vote in cq_label.get('all', []):
1667 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1668 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001669 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001670 if max_cq_vote == 1:
1671 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001672
Aaron Gable9ab38c62017-04-06 14:36:33 -07001673 if data['labels'].get('Code-Review', {}).get('approved'):
1674 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001675
1676 if not data.get('reviewers', {}).get('REVIEWER', []):
1677 return 'unsent'
1678
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001679 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001680 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001681 while messages:
1682 m = messages.pop()
1683 if m.get('tag', '').startswith('autogenerated:cq:'):
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001684 # Ignore replies from CQ.
1685 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001686 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001687 # Most recent message was by owner.
1688 return 'waiting'
1689 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001690 # Some reply from non-owner.
1691 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001692
1693 # Somehow there are no messages even though there are reviewers.
1694 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001695
1696 def GetMostRecentPatchset(self):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001697 if not self.GetIssue():
1698 return None
1699
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001700 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001701 patchset = data['revisions'][data['current_revision']]['_number']
1702 self.SetPatchset(patchset)
1703 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001704
Aaron Gable636b13f2017-07-14 10:42:48 -07001705 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001706 gerrit_util.SetReview(
1707 self._GetGerritHost(), self._GerritChangeIdentifier(),
1708 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001709
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001710 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001711 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001712 # CURRENT_REVISION is included to get the latest patchset so that
1713 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001714 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001715 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1716 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001717 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001718 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001719 robot_file_comments = gerrit_util.GetChangeRobotComments(
1720 self._GetGerritHost(), self._GerritChangeIdentifier())
1721
1722 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001723 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001724 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001725 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001726 line_comments = file_comments.setdefault(path, [])
1727 line_comments.extend(
1728 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001729
1730 # Build dictionary of file comments for easy access and sorting later.
1731 # {author+date: {path: {patchset: {line: url+message}}}}
1732 comments = collections.defaultdict(
1733 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001734 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001735 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001736 tag = comment.get('tag', '')
1737 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001738 continue
1739 key = (comment['author']['email'], comment['updated'])
1740 if comment.get('side', 'REVISION') == 'PARENT':
1741 patchset = 'Base'
1742 else:
1743 patchset = 'PS%d' % comment['patch_set']
1744 line = comment.get('line', 0)
1745 url = ('https://%s/c/%s/%s/%s#%s%s' %
1746 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1747 'b' if comment.get('side') == 'PARENT' else '',
1748 str(line) if line else ''))
1749 comments[key][path][patchset][line] = (url, comment['message'])
1750
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001751 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001752 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001753 summary = self._BuildCommentSummary(msg, comments, readable)
1754 if summary:
1755 summaries.append(summary)
1756 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001757
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001758 @staticmethod
1759 def _BuildCommentSummary(msg, comments, readable):
1760 key = (msg['author']['email'], msg['date'])
1761 # Don't bother showing autogenerated messages that don't have associated
1762 # file or line comments. this will filter out most autogenerated
1763 # messages, but will keep robot comments like those from Tricium.
1764 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1765 if is_autogenerated and not comments.get(key):
1766 return None
1767 message = msg['message']
1768 # Gerrit spits out nanoseconds.
1769 assert len(msg['date'].split('.')[-1]) == 9
1770 date = datetime.datetime.strptime(msg['date'][:-3],
1771 '%Y-%m-%d %H:%M:%S.%f')
1772 if key in comments:
1773 message += '\n'
1774 for path, patchsets in sorted(comments.get(key, {}).items()):
1775 if readable:
1776 message += '\n%s' % path
1777 for patchset, lines in sorted(patchsets.items()):
1778 for line, (url, content) in sorted(lines.items()):
1779 if line:
1780 line_str = 'Line %d' % line
1781 path_str = '%s:%d:' % (path, line)
1782 else:
1783 line_str = 'File comment'
1784 path_str = '%s:0:' % path
1785 if readable:
1786 message += '\n %s, %s: %s' % (patchset, line_str, url)
1787 message += '\n %s\n' % content
1788 else:
1789 message += '\n%s ' % path_str
1790 message += '\n%s\n' % content
1791
1792 return _CommentSummary(
1793 date=date,
1794 message=message,
1795 sender=msg['author']['email'],
1796 autogenerated=is_autogenerated,
1797 # These could be inferred from the text messages and correlated with
1798 # Code-Review label maximum, however this is not reliable.
1799 # Leaving as is until the need arises.
1800 approval=False,
1801 disapproval=False,
1802 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001805 gerrit_util.AbandonChange(
1806 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001808 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001809 gerrit_util.SubmitChange(
1810 self._GetGerritHost(), self._GerritChangeIdentifier(),
1811 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001813 def _GetChangeDetail(self, options=None):
1814 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001815 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001816 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001817
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001818 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001819 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001820 options.append('CURRENT_COMMIT')
1821
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001822 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001823 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001824 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001825
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001826 for cached_options_set, data in self._detail_cache.get(cache_key, []):
1827 # Assumption: data fetched before with extra options is suitable
1828 # for return for a smaller set of options.
1829 # For example, if we cached data for
1830 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
1831 # and request is for options=[CURRENT_REVISION],
1832 # THEN we can return prior cached data.
1833 if options_set.issubset(cached_options_set):
1834 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001835
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001836 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001837 data = gerrit_util.GetChangeDetail(
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001838 self._GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001839 except gerrit_util.GerritError as e:
1840 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001841 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001842 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001843
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001844 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07001845 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001846
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00001847 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001848 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001849 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001850 data = gerrit_util.GetChangeCommit(
1851 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001852 except gerrit_util.GerritError as e:
1853 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001854 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001855 raise
agable32978d92016-11-01 12:55:02 -07001856 return data
1857
Karen Qian40c19422019-03-13 21:28:29 +00001858 def _IsCqConfigured(self):
1859 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00001860 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00001861
Olivier Robin75ee7252018-04-13 10:02:56 +02001862 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001863 if git_common.is_dirty_git_tree('land'):
1864 return 1
Karen Qian40c19422019-03-13 21:28:29 +00001865
tandriid60367b2016-06-22 05:25:12 -07001866 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00001867 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001868 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001869 'which can test and land changes for you. '
1870 'Are you sure you wish to bypass it?\n',
1871 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001872 differs = True
tandriic4344b52016-08-29 06:04:54 -07001873 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001874 # Note: git diff outputs nothing if there is no diff.
1875 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001876 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001877 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001878 if detail['current_revision'] == last_upload:
1879 differs = False
1880 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001881 print('WARNING: Local branch contents differ from latest uploaded '
1882 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001883 if differs:
1884 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001885 confirm_or_exit(
1886 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
1887 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001888 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001889 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00001890 upstream = self.GetCommonAncestorWithUpstream()
1891 if self.GetIssue():
1892 description = self.FetchDescription()
1893 else:
Edward Lemura12175c2020-03-09 16:58:26 +00001894 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00001895 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001896 committing=True,
1897 may_prompt=not force,
1898 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00001899 parallel=parallel,
1900 upstream=upstream,
1901 description=description,
1902 all_files=False)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001903
1904 self.SubmitIssue(wait_for_merge=True)
1905 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07001906 links = self._GetChangeCommit().get('web_links', [])
1907 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08001908 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001909 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07001910 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001911 return 0
1912
Edward Lemurf38bc172019-09-03 21:02:13 +00001913 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001914 assert parsed_issue_arg.valid
1915
Edward Lemur125d60a2019-09-13 18:25:41 +00001916 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001917
1918 if parsed_issue_arg.hostname:
1919 self._gerrit_host = parsed_issue_arg.hostname
1920 self._gerrit_server = 'https://%s' % self._gerrit_host
1921
tandriic2405f52016-10-10 08:13:15 -07001922 try:
1923 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08001924 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07001925 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001926
1927 if not parsed_issue_arg.patchset:
1928 # Use current revision by default.
1929 revision_info = detail['revisions'][detail['current_revision']]
1930 patchset = int(revision_info['_number'])
1931 else:
1932 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001933 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001934 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1935 break
1936 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08001937 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001938 (parsed_issue_arg.patchset, self.GetIssue()))
1939
Edward Lemur125d60a2019-09-13 18:25:41 +00001940 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08001941 if remote_url.endswith('.git'):
1942 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00001943 remote_url = remote_url.rstrip('/')
1944
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001945 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00001946 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08001947
1948 if remote_url != fetch_info['url']:
1949 DieWithError('Trying to patch a change from %s but this repo appears '
1950 'to be %s.' % (fetch_info['url'], remote_url))
1951
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001952 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07001953
Aaron Gable62619a32017-06-16 08:22:09 -07001954 if force:
1955 RunGit(['reset', '--hard', 'FETCH_HEAD'])
1956 print('Checked out commit for change %i patchset %i locally' %
1957 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07001958 elif nocommit:
1959 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
1960 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07001961 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07001962 RunGit(['cherry-pick', 'FETCH_HEAD'])
1963 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07001964 (parsed_issue_arg.issue, patchset))
1965 print('Note: this created a local commit which does not have '
1966 'the same hash as the one uploaded for review. This will make '
1967 'uploading changes based on top of this branch difficult.\n'
1968 'If you want to do that, use "git cl patch --force" instead.')
1969
Stefan Zagerd08043c2017-10-12 12:07:02 -07001970 if self.GetBranch():
1971 self.SetIssue(parsed_issue_arg.issue)
1972 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001973 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Stefan Zagerd08043c2017-10-12 12:07:02 -07001974 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
1975 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
1976 else:
1977 print('WARNING: You are in detached HEAD state.\n'
1978 'The patch has been applied to your checkout, but you will not be '
1979 'able to upload a new patch set to the gerrit issue.\n'
1980 'Try using the \'-b\' option if you would like to work on a '
1981 'branch and/or upload a new patch set.')
1982
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001983 return 0
1984
tandrii16e0b4e2016-06-07 10:34:28 -07001985 def _GerritCommitMsgHookCheck(self, offer_removal):
1986 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1987 if not os.path.exists(hook):
1988 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001989 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
1990 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07001991 data = gclient_utils.FileRead(hook)
1992 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
1993 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001994 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07001995 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07001996 'and may interfere with it in subtle ways.\n'
1997 'We recommend you remove the commit-msg hook.')
1998 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001999 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002000 gclient_utils.rm_file_or_tree(hook)
2001 print('Gerrit commit-msg hook removed.')
2002 else:
2003 print('OK, will keep Gerrit commit-msg hook in place.')
2004
Edward Lemur1b52d872019-05-09 21:12:12 +00002005 def _CleanUpOldTraces(self):
2006 """Keep only the last |MAX_TRACES| traces."""
2007 try:
2008 traces = sorted([
2009 os.path.join(TRACES_DIR, f)
2010 for f in os.listdir(TRACES_DIR)
2011 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2012 and not f.startswith('tmp'))
2013 ])
2014 traces_to_delete = traces[:-MAX_TRACES]
2015 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002016 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002017 except OSError:
2018 print('WARNING: Failed to remove old git traces from\n'
2019 ' %s'
2020 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002021
Edward Lemur5737f022019-05-17 01:24:00 +00002022 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002023 """Zip and write the git push traces stored in traces_dir."""
2024 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002025 traces_zip = trace_name + '-traces'
2026 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002027 # Create a temporary dir to store git config and gitcookies in. It will be
2028 # compressed and stored next to the traces.
2029 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002030 git_info_zip = trace_name + '-git-info'
2031
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002032 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002033
Edward Lemur1b52d872019-05-09 21:12:12 +00002034 git_push_metadata['trace_name'] = trace_name
2035 gclient_utils.FileWrite(
2036 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2037
2038 # Keep only the first 6 characters of the git hashes on the packet
2039 # trace. This greatly decreases size after compression.
2040 packet_traces = os.path.join(traces_dir, 'trace-packet')
2041 if os.path.isfile(packet_traces):
2042 contents = gclient_utils.FileRead(packet_traces)
2043 gclient_utils.FileWrite(
2044 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2045 shutil.make_archive(traces_zip, 'zip', traces_dir)
2046
2047 # Collect and compress the git config and gitcookies.
2048 git_config = RunGit(['config', '-l'])
2049 gclient_utils.FileWrite(
2050 os.path.join(git_info_dir, 'git-config'),
2051 git_config)
2052
2053 cookie_auth = gerrit_util.Authenticator.get()
2054 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2055 gitcookies_path = cookie_auth.get_gitcookies_path()
2056 if os.path.isfile(gitcookies_path):
2057 gitcookies = gclient_utils.FileRead(gitcookies_path)
2058 gclient_utils.FileWrite(
2059 os.path.join(git_info_dir, 'gitcookies'),
2060 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2061 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2062
Edward Lemur1b52d872019-05-09 21:12:12 +00002063 gclient_utils.rmtree(git_info_dir)
2064
2065 def _RunGitPushWithTraces(
2066 self, change_desc, refspec, refspec_opts, git_push_metadata):
2067 """Run git push and collect the traces resulting from the execution."""
2068 # Create a temporary directory to store traces in. Traces will be compressed
2069 # and stored in a 'traces' dir inside depot_tools.
2070 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002071 trace_name = os.path.join(
2072 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002073
2074 env = os.environ.copy()
2075 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2076 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002077 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002078 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2079 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2080 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2081
2082 try:
2083 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002084 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002085 before_push = time_time()
2086 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002087 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002088 env=env,
2089 print_stdout=True,
2090 # Flush after every line: useful for seeing progress when running as
2091 # recipe.
2092 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002093 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002094 except subprocess2.CalledProcessError as e:
2095 push_returncode = e.returncode
2096 DieWithError('Failed to create a change. Please examine output above '
2097 'for the reason of the failure.\n'
2098 'Hint: run command below to diagnose common Git/Gerrit '
2099 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002100 ' git cl creds-check\n'
2101 '\n'
2102 'If git-cl is not working correctly, file a bug under the '
2103 'Infra>SDK component including the files below.\n'
2104 'Review the files before upload, since they might contain '
2105 'sensitive information.\n'
2106 'Set the Restrict-View-Google label so that they are not '
2107 'publicly accessible.\n'
2108 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002109 change_desc)
2110 finally:
2111 execution_time = time_time() - before_push
2112 metrics.collector.add_repeated('sub_commands', {
2113 'command': 'git push',
2114 'execution_time': execution_time,
2115 'exit_code': push_returncode,
2116 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2117 })
2118
Edward Lemur1b52d872019-05-09 21:12:12 +00002119 git_push_metadata['execution_time'] = execution_time
2120 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002121 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002122
Edward Lemur1b52d872019-05-09 21:12:12 +00002123 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002124 gclient_utils.rmtree(traces_dir)
2125
2126 return push_stdout
2127
Edward Lemura12175c2020-03-09 16:58:26 +00002128 def CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00002129 self, options, git_diff_args, custom_cl_base, change_desc):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002130 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002131 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002132 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002133
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002134 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002135 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002136 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002137 # User requested to change description
2138 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002139 change_desc.prompt()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002140 change_id = self._GetChangeDetail()['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002141 change_desc.ensure_change_id(change_id)
Aaron Gableb56ad332017-01-06 15:24:31 -08002142 else: # if not self.GetIssue()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002144 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002145 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002146 if len(change_ids) == 1:
2147 change_id = change_ids[0]
2148 else:
2149 change_id = GenerateGerritChangeId(change_desc.description)
2150 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002151
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002152 if options.preserve_tryjobs:
2153 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002154
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002155 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Edward Lemur5a644f82020-03-18 16:44:57 +00002156 parent = self._ComputeParent(
2157 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002159 with gclient_utils.temporary_file() as desc_tempfile:
2160 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2161 ref_to_push = RunGit(
2162 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002163 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002164 if not git_footers.get_footer_change_id(change_desc.description):
2165 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002166 change_desc.set_description(
Edward Lemur5a644f82020-03-18 16:44:57 +00002167 self._AddChangeIdToCommitMessage(
2168 change_desc.description, git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002169 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002170 # For no-squash mode, we assume the remote called "origin" is the one we
2171 # want. It is not worthwhile to support different workflows for
2172 # no-squash mode.
2173 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002174 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2175
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002176 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002177 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2178 ref_to_push)]).splitlines()
2179 if len(commits) > 1:
2180 print('WARNING: This will upload %d commits. Run the following command '
2181 'to see which commits will be uploaded: ' % len(commits))
2182 print('git log %s..%s' % (parent, ref_to_push))
2183 print('You can also use `git squash-branch` to squash these into a '
2184 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002185 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002186
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002187 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002188 cc = []
2189 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2190 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2191 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002192 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002193 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002194 if options.cc:
2195 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002196 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002197 if change_desc.get_cced():
2198 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002199 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2200 valid_accounts = set(reviewers + cc)
2201 # TODO(crbug/877717): relax this for all hosts.
2202 else:
2203 valid_accounts = gerrit_util.ValidAccounts(
2204 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002205 logging.info('accounts %s are recognized, %s invalid',
2206 sorted(valid_accounts),
2207 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002208
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002209 # Extra options that can be specified at push time. Doc:
2210 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002211 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002212
Aaron Gable844cf292017-06-28 11:32:59 -07002213 # By default, new changes are started in WIP mode, and subsequent patchsets
2214 # don't send email. At any time, passing --send-mail will mark the change
2215 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002216 if options.send_mail:
2217 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002218 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002219 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002220 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002221 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002222 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002223
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002224 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002225 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002226
Edward Lemur5a644f82020-03-18 16:44:57 +00002227 title = self._GetTitleForUpload(options)
Aaron Gable9b713dd2016-12-14 16:04:21 -08002228 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002229 # Punctuation and whitespace in |title| must be percent-encoded.
2230 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002231
agablec6787972016-09-09 16:13:34 -07002232 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002233 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002234
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002235 for r in sorted(reviewers):
2236 if r in valid_accounts:
2237 refspec_opts.append('r=%s' % r)
2238 reviewers.remove(r)
2239 else:
2240 # TODO(tandrii): this should probably be a hard failure.
2241 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2242 % r)
2243 for c in sorted(cc):
2244 # refspec option will be rejected if cc doesn't correspond to an
2245 # account, even though REST call to add such arbitrary cc may succeed.
2246 if c in valid_accounts:
2247 refspec_opts.append('cc=%s' % c)
2248 cc.remove(c)
2249
rmistry9eadede2016-09-19 11:22:43 -07002250 if options.topic:
2251 # Documentation on Gerrit topics is here:
2252 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002253 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002254
Edward Lemur687ca902018-12-05 02:30:30 +00002255 if options.enable_auto_submit:
2256 refspec_opts.append('l=Auto-Submit+1')
2257 if options.use_commit_queue:
2258 refspec_opts.append('l=Commit-Queue+2')
2259 elif options.cq_dry_run:
2260 refspec_opts.append('l=Commit-Queue+1')
2261
2262 if change_desc.get_reviewers(tbr_only=True):
2263 score = gerrit_util.GetCodeReviewTbrScore(
2264 self._GetGerritHost(),
2265 self._GetGerritProject())
2266 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002267
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002268 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002269 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002270 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002271 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002272 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2273
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002274 refspec_suffix = ''
2275 if refspec_opts:
2276 refspec_suffix = '%' + ','.join(refspec_opts)
2277 assert ' ' not in refspec_suffix, (
2278 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2279 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2280
Edward Lemur1b52d872019-05-09 21:12:12 +00002281 git_push_metadata = {
2282 'gerrit_host': self._GetGerritHost(),
2283 'title': title or '<untitled>',
2284 'change_id': change_id,
2285 'description': change_desc.description,
2286 }
2287 push_stdout = self._RunGitPushWithTraces(
2288 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002289
2290 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002291 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002292 change_numbers = [m.group(1)
2293 for m in map(regex.match, push_stdout.splitlines())
2294 if m]
2295 if len(change_numbers) != 1:
2296 DieWithError(
2297 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002298 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002300 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002301
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002302 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002303 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002304 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002305 gerrit_util.AddReviewers(
2306 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002307 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002308 reviewers, cc,
2309 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002310
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 return 0
2312
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002313 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2314 change_desc):
2315 """Computes parent of the generated commit to be uploaded to Gerrit.
2316
2317 Returns revision or a ref name.
2318 """
2319 if custom_cl_base:
2320 # Try to avoid creating additional unintended CLs when uploading, unless
2321 # user wants to take this risk.
2322 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2323 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2324 local_ref_of_target_remote])
2325 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002326 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002327 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2328 'If you proceed with upload, more than 1 CL may be created by '
2329 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2330 'If you are certain that specified base `%s` has already been '
2331 'uploaded to Gerrit as another CL, you may proceed.\n' %
2332 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2333 if not force:
2334 confirm_or_exit(
2335 'Do you take responsibility for cleaning up potential mess '
2336 'resulting from proceeding with upload?',
2337 action='upload')
2338 return custom_cl_base
2339
Aaron Gablef97e33d2017-03-30 15:44:27 -07002340 if remote != '.':
2341 return self.GetCommonAncestorWithUpstream()
2342
2343 # If our upstream branch is local, we base our squashed commit on its
2344 # squashed version.
2345 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2346
Aaron Gablef97e33d2017-03-30 15:44:27 -07002347 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002348 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002349
2350 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002351 # TODO(tandrii): consider checking parent change in Gerrit and using its
2352 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2353 # the tree hash of the parent branch. The upside is less likely bogus
2354 # requests to reupload parent change just because it's uploadhash is
2355 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Edward Lesmesa680c232020-03-31 18:26:44 +00002356 parent = scm.GIT.GetBranchConfig(
2357 settings.GetRoot(), upstream_branch_name, 'gerritsquashhash')
Aaron Gablef97e33d2017-03-30 15:44:27 -07002358 # Verify that the upstream branch has been uploaded too, otherwise
2359 # Gerrit will create additional CLs when uploading.
2360 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2361 RunGitSilent(['rev-parse', parent + ':'])):
2362 DieWithError(
2363 '\nUpload upstream branch %s first.\n'
2364 'It is likely that this branch has been rebased since its last '
2365 'upload, so you just need to upload it again.\n'
2366 '(If you uploaded it with --no-squash, then branch dependencies '
2367 'are not supported, and you should reupload with --squash.)'
2368 % upstream_branch_name,
2369 change_desc)
2370 return parent
2371
Edward Lemura12175c2020-03-09 16:58:26 +00002372 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002373 """Re-commits using the current message, assumes the commit hook is in
2374 place.
2375 """
Edward Lemura12175c2020-03-09 16:58:26 +00002376 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002377 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002378 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002379 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002380 return new_log_desc
2381 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002382 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002383
tandriie113dfd2016-10-11 10:20:12 -07002384 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002385 try:
2386 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002387 except GerritChangeNotExists:
2388 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002389
2390 if data['status'] in ('ABANDONED', 'MERGED'):
2391 return 'CL %s is closed' % self.GetIssue()
2392
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002393 def GetGerritChange(self, patchset=None):
2394 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002395 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002396 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002397 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002398 data = self._GetChangeDetail(['ALL_REVISIONS'])
2399
2400 assert host and issue and patchset, 'CL must be uploaded first'
2401
2402 has_patchset = any(
2403 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002404 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002405 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002406 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002407 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002408
tandrii8c5a3532016-11-04 07:52:02 -07002409 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002410 'host': host,
2411 'change': issue,
2412 'project': data['project'],
2413 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002414 }
tandriie113dfd2016-10-11 10:20:12 -07002415
tandriide281ae2016-10-12 06:02:30 -07002416 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002417 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002418
Edward Lemur707d70b2018-02-07 00:50:14 +01002419 def GetReviewers(self):
2420 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002421 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002422
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002423
tandriif9aefb72016-07-01 09:06:51 -07002424def _get_bug_line_values(default_project, bugs):
2425 """Given default_project and comma separated list of bugs, yields bug line
2426 values.
2427
2428 Each bug can be either:
2429 * a number, which is combined with default_project
2430 * string, which is left as is.
2431
2432 This function may produce more than one line, because bugdroid expects one
2433 project per line.
2434
2435 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2436 ['v8:123', 'chromium:789']
2437 """
2438 default_bugs = []
2439 others = []
2440 for bug in bugs.split(','):
2441 bug = bug.strip()
2442 if bug:
2443 try:
2444 default_bugs.append(int(bug))
2445 except ValueError:
2446 others.append(bug)
2447
2448 if default_bugs:
2449 default_bugs = ','.join(map(str, default_bugs))
2450 if default_project:
2451 yield '%s:%s' % (default_project, default_bugs)
2452 else:
2453 yield default_bugs
2454 for other in sorted(others):
2455 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2456 yield other
2457
2458
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002459class ChangeDescription(object):
2460 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002461 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002462 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002463 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002464 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002465 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002466 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2467 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002468 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002469 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002470
Dan Beamd8b04ca2019-10-10 21:23:26 +00002471 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002472 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002473 if bug:
2474 regexp = re.compile(self.BUG_LINE)
2475 prefix = settings.GetBugPrefix()
2476 if not any((regexp.match(line) for line in self._description_lines)):
2477 values = list(_get_bug_line_values(prefix, bug))
2478 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002479 if fixed:
2480 regexp = re.compile(self.FIXED_LINE)
2481 prefix = settings.GetBugPrefix()
2482 if not any((regexp.match(line) for line in self._description_lines)):
2483 values = list(_get_bug_line_values(prefix, fixed))
2484 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002485
agable@chromium.org42c20792013-09-12 17:34:49 +00002486 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002487 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002488 return '\n'.join(self._description_lines)
2489
2490 def set_description(self, desc):
2491 if isinstance(desc, basestring):
2492 lines = desc.splitlines()
2493 else:
2494 lines = [line.rstrip() for line in desc]
2495 while lines and not lines[0]:
2496 lines.pop(0)
2497 while lines and not lines[-1]:
2498 lines.pop(-1)
2499 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002500
Edward Lemur5a644f82020-03-18 16:44:57 +00002501 def ensure_change_id(self, change_id):
2502 description = self.description
2503 footer_change_ids = git_footers.get_footer_change_id(description)
2504 # Make sure that the Change-Id in the description matches the given one.
2505 if footer_change_ids != [change_id]:
2506 if footer_change_ids:
2507 # Remove any existing Change-Id footers since they don't match the
2508 # expected change_id footer.
2509 description = git_footers.remove_footer(description, 'Change-Id')
2510 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
2511 'if you want to set a new one.')
2512 # Add the expected Change-Id footer.
2513 description = git_footers.add_footer_change_id(description, change_id)
2514 self.set_description(description)
2515
Edward Lemur2c62b332020-03-12 22:12:33 +00002516 def update_reviewers(
2517 self, reviewers, tbrs, add_owners_to, affected_files, author_email):
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002518 """Rewrites the R=/TBR= line(s) as a single line each.
2519
2520 Args:
2521 reviewers (list(str)) - list of additional emails to use for reviewers.
2522 tbrs (list(str)) - list of additional emails to use for TBRs.
2523 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2524 the change that are missing OWNER coverage. If this is not None, you
2525 must also pass a value for `change`.
2526 change (Change) - The Change that should be used for OWNERS lookups.
2527 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002528 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002529 assert isinstance(tbrs, list), tbrs
2530
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002531 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Edward Lemur2c62b332020-03-12 22:12:33 +00002532 assert not add_owners_to or affected_files, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002533
2534 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002535 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002536
2537 reviewers = set(reviewers)
2538 tbrs = set(tbrs)
2539 LOOKUP = {
2540 'TBR': tbrs,
2541 'R': reviewers,
2542 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002543
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002544 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002545 regexp = re.compile(self.R_LINE)
2546 matches = [regexp.match(line) for line in self._description_lines]
2547 new_desc = [l for i, l in enumerate(self._description_lines)
2548 if not matches[i]]
2549 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002550
agable@chromium.org42c20792013-09-12 17:34:49 +00002551 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002552
2553 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002554 for match in matches:
2555 if not match:
2556 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002557 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2558
2559 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002560 if add_owners_to:
Edward Lemur2c62b332020-03-12 22:12:33 +00002561 owners_db = owners.Database(settings.GetRoot(),
Edward Lemurb7f759f2020-03-04 21:20:56 +00002562 fopen=open, os_path=os.path)
Edward Lemur2c62b332020-03-12 22:12:33 +00002563 missing_files = owners_db.files_not_covered_by(affected_files,
Robert Iannucci100aa212017-04-18 17:28:26 -07002564 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002565 LOOKUP[add_owners_to].update(
Edward Lemur2c62b332020-03-12 22:12:33 +00002566 owners_db.reviewers_for(missing_files, author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002567
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002568 # If any folks ended up in both groups, remove them from tbrs.
2569 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002570
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002571 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2572 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002573
2574 # Put the new lines in the description where the old first R= line was.
2575 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2576 if 0 <= line_loc < len(self._description_lines):
2577 if new_tbr_line:
2578 self._description_lines.insert(line_loc, new_tbr_line)
2579 if new_r_line:
2580 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002581 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002582 if new_r_line:
2583 self.append_footer(new_r_line)
2584 if new_tbr_line:
2585 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002587 def set_preserve_tryjobs(self):
2588 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2589 footers = git_footers.parse_footers(self.description)
2590 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2591 if v.lower() == 'true':
2592 return
2593 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2594
Anthony Polito8b955342019-09-24 19:01:36 +00002595 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002596 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002597 self.set_description([
2598 '# Enter a description of the change.',
2599 '# This will be displayed on the codereview site.',
2600 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002601 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002602 '--------------------',
2603 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002604 bug_regexp = re.compile(self.BUG_LINE)
2605 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002606 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002607 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2608 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002609 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002610
agable@chromium.org42c20792013-09-12 17:34:49 +00002611 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002612 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002613 if not content:
2614 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002615 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002616
Bruce Dawson2377b012018-01-11 16:46:49 -08002617 # Strip off comments and default inserted "Bug:" line.
2618 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002619 (line.startswith('#') or
2620 line.rstrip() == "Bug:" or
2621 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002622 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002623 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002624 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002625
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002626 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002627 """Adds a footer line to the description.
2628
2629 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2630 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2631 that Gerrit footers are always at the end.
2632 """
2633 parsed_footer_line = git_footers.parse_footer(line)
2634 if parsed_footer_line:
2635 # Line is a gerrit footer in the form: Footer-Key: any value.
2636 # Thus, must be appended observing Gerrit footer rules.
2637 self.set_description(
2638 git_footers.add_footer(self.description,
2639 key=parsed_footer_line[0],
2640 value=parsed_footer_line[1]))
2641 return
2642
2643 if not self._description_lines:
2644 self._description_lines.append(line)
2645 return
2646
2647 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2648 if gerrit_footers:
2649 # git_footers.split_footers ensures that there is an empty line before
2650 # actual (gerrit) footers, if any. We have to keep it that way.
2651 assert top_lines and top_lines[-1] == ''
2652 top_lines, separator = top_lines[:-1], top_lines[-1:]
2653 else:
2654 separator = [] # No need for separator if there are no gerrit_footers.
2655
2656 prev_line = top_lines[-1] if top_lines else ''
2657 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2658 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2659 top_lines.append('')
2660 top_lines.append(line)
2661 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002662
tandrii99a72f22016-08-17 14:33:24 -07002663 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002664 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002665 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002666 reviewers = [match.group(2).strip()
2667 for match in matches
2668 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002669 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002670
bradnelsond975b302016-10-23 12:20:23 -07002671 def get_cced(self):
2672 """Retrieves the list of reviewers."""
2673 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2674 cced = [match.group(2).strip() for match in matches if match]
2675 return cleanup_list(cced)
2676
Nodir Turakulov23b82142017-11-16 11:04:25 -08002677 def get_hash_tags(self):
2678 """Extracts and sanitizes a list of Gerrit hashtags."""
2679 subject = (self._description_lines or ('',))[0]
2680 subject = re.sub(
2681 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2682
2683 tags = []
2684 start = 0
2685 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2686 while True:
2687 m = bracket_exp.match(subject, start)
2688 if not m:
2689 break
2690 tags.append(self.sanitize_hash_tag(m.group(1)))
2691 start = m.end()
2692
2693 if not tags:
2694 # Try "Tag: " prefix.
2695 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
2696 if m:
2697 tags.append(self.sanitize_hash_tag(m.group(1)))
2698 return tags
2699
2700 @classmethod
2701 def sanitize_hash_tag(cls, tag):
2702 """Returns a sanitized Gerrit hash tag.
2703
2704 A sanitized hashtag can be used as a git push refspec parameter value.
2705 """
2706 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
2707
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709def FindCodereviewSettingsFile(filename='codereview.settings'):
2710 """Finds the given file starting in the cwd and going up.
2711
2712 Only looks up to the top of the repository unless an
2713 'inherit-review-settings-ok' file exists in the root of the repository.
2714 """
2715 inherit_ok_file = 'inherit-review-settings-ok'
2716 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002717 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2719 root = '/'
2720 while True:
2721 if filename in os.listdir(cwd):
2722 if os.path.isfile(os.path.join(cwd, filename)):
2723 return open(os.path.join(cwd, filename))
2724 if cwd == root:
2725 break
2726 cwd = os.path.dirname(cwd)
2727
2728
2729def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002730 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002731 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002732
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002733 def SetProperty(name, setting, unset_error_ok=False):
2734 fullname = 'rietveld.' + name
2735 if setting in keyvals:
2736 RunGit(['config', fullname, keyvals[setting]])
2737 else:
2738 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2739
tandrii48df5812016-10-17 03:55:37 -07002740 if not keyvals.get('GERRIT_HOST', False):
2741 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002742 # Only server setting is required. Other settings can be absent.
2743 # In that case, we ignore errors raised during option deletion attempt.
2744 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
2745 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2746 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002747 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002748 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
2749 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002750 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2751 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00002752 SetProperty(
2753 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002754
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002755 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002756 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002757
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002758 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00002759 RunGit(['config', 'gerrit.squash-uploads',
2760 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002761
tandrii@chromium.org28253532016-04-14 13:46:56 +00002762 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002763 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002764 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002766 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002767 # should be of the form
2768 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2769 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002770 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2771 keyvals['ORIGIN_URL_CONFIG']])
2772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002774def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002775 """Downloads a network object to a local file, like urllib.urlretrieve.
2776
2777 This is necessary because urllib is broken for SSL connections via a proxy.
2778 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002779 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00002780 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002781
2782
ukai@chromium.org712d6102013-11-27 00:52:58 +00002783def hasSheBang(fname):
2784 """Checks fname is a #! script."""
2785 with open(fname) as f:
2786 return f.read(2).startswith('#!')
2787
2788
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002789def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002790 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002791
2792 Args:
2793 force: True to update hooks. False to install hooks if not present.
2794 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00002795 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002796 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2797 if not os.access(dst, os.X_OK):
2798 if os.path.exists(dst):
2799 if not force:
2800 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002801 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002802 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002803 if not hasSheBang(dst):
2804 DieWithError('Not a script: %s\n'
2805 'You need to download from\n%s\n'
2806 'into .git/hooks/commit-msg and '
2807 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002808 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2809 except Exception:
2810 if os.path.exists(dst):
2811 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002812 DieWithError('\nFailed to download hooks.\n'
2813 'You need to download from\n%s\n'
2814 'into .git/hooks/commit-msg and '
2815 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002816
2817
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002818class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002819 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002820
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002821 _GOOGLESOURCE = 'googlesource.com'
2822
2823 def __init__(self):
2824 # Cached list of [host, identity, source], where source is either
2825 # .gitcookies or .netrc.
2826 self._all_hosts = None
2827
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002828 def ensure_configured_gitcookies(self):
2829 """Runs checks and suggests fixes to make git use .gitcookies from default
2830 path."""
2831 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
2832 configured_path = RunGitSilent(
2833 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02002834 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002835 if configured_path:
2836 self._ensure_default_gitcookies_path(configured_path, default)
2837 else:
2838 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002839
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002840 @staticmethod
2841 def _ensure_default_gitcookies_path(configured_path, default_path):
2842 assert configured_path
2843 if configured_path == default_path:
2844 print('git is already configured to use your .gitcookies from %s' %
2845 configured_path)
2846 return
2847
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002848 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002849 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
2850 (configured_path, default_path))
2851
2852 if not os.path.exists(configured_path):
2853 print('However, your configured .gitcookies file is missing.')
2854 confirm_or_exit('Reconfigure git to use default .gitcookies?',
2855 action='reconfigure')
2856 RunGit(['config', '--global', 'http.cookiefile', default_path])
2857 return
2858
2859 if os.path.exists(default_path):
2860 print('WARNING: default .gitcookies file already exists %s' %
2861 default_path)
2862 DieWithError('Please delete %s manually and re-run git cl creds-check' %
2863 default_path)
2864
2865 confirm_or_exit('Move existing .gitcookies to default location?',
2866 action='move')
2867 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002868 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002869 print('Moved and reconfigured git to use .gitcookies from %s' %
2870 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002871
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002872 @staticmethod
2873 def _configure_gitcookies_path(default_path):
2874 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
2875 if os.path.exists(netrc_path):
2876 print('You seem to be using outdated .netrc for git credentials: %s' %
2877 netrc_path)
2878 print('This tool will guide you through setting up recommended '
2879 '.gitcookies store for git credentials.\n'
2880 '\n'
2881 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
2882 ' git config --global --unset http.cookiefile\n'
2883 ' mv %s %s.backup\n\n' % (default_path, default_path))
2884 confirm_or_exit(action='setup .gitcookies')
2885 RunGit(['config', '--global', 'http.cookiefile', default_path])
2886 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002887
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002888 def get_hosts_with_creds(self, include_netrc=False):
2889 if self._all_hosts is None:
2890 a = gerrit_util.CookiesAuthenticator()
2891 self._all_hosts = [
2892 (h, u, s)
2893 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002894 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
2895 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002896 )
2897 if h.endswith(self._GOOGLESOURCE)
2898 ]
2899
2900 if include_netrc:
2901 return self._all_hosts
2902 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
2903
2904 def print_current_creds(self, include_netrc=False):
2905 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
2906 if not hosts:
2907 print('No Git/Gerrit credentials found')
2908 return
Edward Lemur79d4f992019-11-11 23:49:02 +00002909 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002910 header = [('Host', 'User', 'Which file'),
2911 ['=' * l for l in lengths]]
2912 for row in (header + hosts):
2913 print('\t'.join((('%%+%ds' % l) % s)
2914 for l, s in zip(lengths, row)))
2915
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002916 @staticmethod
2917 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08002918 """Parses identity "git-<username>.domain" into <username> and domain."""
2919 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002920 # distinguishable from sub-domains. But we do know typical domains:
2921 if identity.endswith('.chromium.org'):
2922 domain = 'chromium.org'
2923 username = identity[:-len('.chromium.org')]
2924 else:
2925 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002926 if username.startswith('git-'):
2927 username = username[len('git-'):]
2928 return username, domain
2929
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002930 def _canonical_git_googlesource_host(self, host):
2931 """Normalizes Gerrit hosts (with '-review') to Git host."""
2932 assert host.endswith(self._GOOGLESOURCE)
2933 # Prefix doesn't include '.' at the end.
2934 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
2935 if prefix.endswith('-review'):
2936 prefix = prefix[:-len('-review')]
2937 return prefix + '.' + self._GOOGLESOURCE
2938
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01002939 def _canonical_gerrit_googlesource_host(self, host):
2940 git_host = self._canonical_git_googlesource_host(host)
2941 prefix = git_host.split('.', 1)[0]
2942 return prefix + '-review.' + self._GOOGLESOURCE
2943
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002944 def _get_counterpart_host(self, host):
2945 assert host.endswith(self._GOOGLESOURCE)
2946 git = self._canonical_git_googlesource_host(host)
2947 gerrit = self._canonical_gerrit_googlesource_host(git)
2948 return git if gerrit == host else gerrit
2949
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002950 def has_generic_host(self):
2951 """Returns whether generic .googlesource.com has been configured.
2952
2953 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
2954 """
2955 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
2956 if host == '.' + self._GOOGLESOURCE:
2957 return True
2958 return False
2959
2960 def _get_git_gerrit_identity_pairs(self):
2961 """Returns map from canonic host to pair of identities (Git, Gerrit).
2962
2963 One of identities might be None, meaning not configured.
2964 """
2965 host_to_identity_pairs = {}
2966 for host, identity, _ in self.get_hosts_with_creds():
2967 canonical = self._canonical_git_googlesource_host(host)
2968 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
2969 idx = 0 if canonical == host else 1
2970 pair[idx] = identity
2971 return host_to_identity_pairs
2972
2973 def get_partially_configured_hosts(self):
2974 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002975 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002976 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002977 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002978
2979 def get_conflicting_hosts(self):
2980 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002981 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002982 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002983 if None not in (i1, i2) and i1 != i2)
2984
2985 def get_duplicated_hosts(self):
2986 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002987 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002988
2989 _EXPECTED_HOST_IDENTITY_DOMAINS = {
2990 'chromium.googlesource.com': 'chromium.org',
2991 'chrome-internal.googlesource.com': 'google.com',
2992 }
2993
2994 def get_hosts_with_wrong_identities(self):
2995 """Finds hosts which **likely** reference wrong identities.
2996
2997 Note: skips hosts which have conflicting identities for Git and Gerrit.
2998 """
2999 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003000 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003001 pair = self._get_git_gerrit_identity_pairs().get(host)
3002 if pair and pair[0] == pair[1]:
3003 _, domain = self._parse_identity(pair[0])
3004 if domain != expected:
3005 hosts.add(host)
3006 return hosts
3007
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003008 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003009 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003010 hosts = sorted(hosts)
3011 assert hosts
3012 if extra_column_func is None:
3013 extras = [''] * len(hosts)
3014 else:
3015 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003016 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3017 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003018 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003019 lines.append(tmpl % he)
3020 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003021
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003022 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003023 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003024 yield ('.googlesource.com wildcard record detected',
3025 ['Chrome Infrastructure team recommends to list full host names '
3026 'explicitly.'],
3027 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003028
3029 dups = self.get_duplicated_hosts()
3030 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003031 yield ('The following hosts were defined twice',
3032 self._format_hosts(dups),
3033 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003034
3035 partial = self.get_partially_configured_hosts()
3036 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003037 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3038 'These hosts are missing',
3039 self._format_hosts(partial, lambda host: 'but %s defined' %
3040 self._get_counterpart_host(host)),
3041 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003042
3043 conflicting = self.get_conflicting_hosts()
3044 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003045 yield ('The following Git hosts have differing credentials from their '
3046 'Gerrit counterparts',
3047 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3048 tuple(self._get_git_gerrit_identity_pairs()[host])),
3049 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003050
3051 wrong = self.get_hosts_with_wrong_identities()
3052 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003053 yield ('These hosts likely use wrong identity',
3054 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3055 (self._get_git_gerrit_identity_pairs()[host][0],
3056 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3057 wrong)
3058
3059 def find_and_report_problems(self):
3060 """Returns True if there was at least one problem, else False."""
3061 found = False
3062 bad_hosts = set()
3063 for title, sublines, hosts in self._find_problems():
3064 if not found:
3065 found = True
3066 print('\n\n.gitcookies problem report:\n')
3067 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003068 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003069 if sublines:
3070 print()
3071 print(' %s' % '\n '.join(sublines))
3072 print()
3073
3074 if bad_hosts:
3075 assert found
3076 print(' You can manually remove corresponding lines in your %s file and '
3077 'visit the following URLs with correct account to generate '
3078 'correct credential lines:\n' %
3079 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3080 print(' %s' % '\n '.join(sorted(set(
3081 gerrit_util.CookiesAuthenticator().get_new_password_url(
3082 self._canonical_git_googlesource_host(host))
3083 for host in bad_hosts
3084 ))))
3085 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003086
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003087
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003088@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003089def CMDcreds_check(parser, args):
3090 """Checks credentials and suggests changes."""
3091 _, _ = parser.parse_args(args)
3092
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003093 # Code below checks .gitcookies. Abort if using something else.
3094 authn = gerrit_util.Authenticator.get()
3095 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003096 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003097 'This command is not designed for bot environment. It checks '
3098 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003099 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3100 if isinstance(authn, gerrit_util.GceAuthenticator):
3101 message += (
3102 '\n'
3103 'If you need to run this on GCE or a cloudtop instance, '
3104 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3105 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003106
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003107 checker = _GitCookiesChecker()
3108 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003109
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003110 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003111 checker.print_current_creds(include_netrc=True)
3112
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003113 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003114 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003115 return 0
3116 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003117
3118
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003119@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003120def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003121 """Gets or sets base-url for this branch."""
Edward Lesmes50da7702020-03-30 19:23:43 +00003122 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003123 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003124 _, args = parser.parse_args(args)
3125 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003126 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003127 return RunGit(['config', 'branch.%s.base-url' % branch],
3128 error_ok=False).strip()
3129 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003130 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003131 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3132 error_ok=False).strip()
3133
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003134
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003135def color_for_status(status):
3136 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003137 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003138 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003139 'unsent': BOLD + Fore.YELLOW,
3140 'waiting': BOLD + Fore.RED,
3141 'reply': BOLD + Fore.YELLOW,
3142 'not lgtm': BOLD + Fore.RED,
3143 'lgtm': BOLD + Fore.GREEN,
3144 'commit': BOLD + Fore.MAGENTA,
3145 'closed': BOLD + Fore.CYAN,
3146 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003147 }.get(status, Fore.WHITE)
3148
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003149
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003150def get_cl_statuses(changes, fine_grained, max_processes=None):
3151 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003152
3153 If fine_grained is true, this will fetch CL statuses from the server.
3154 Otherwise, simply indicate if there's a matching url for the given branches.
3155
3156 If max_processes is specified, it is used as the maximum number of processes
3157 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3158 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003159
3160 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003161 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003162 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003163 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003164
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003165 if not fine_grained:
3166 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003167 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003168 for cl in changes:
3169 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003170 return
3171
3172 # First, sort out authentication issues.
3173 logging.debug('ensuring credentials exist')
3174 for cl in changes:
3175 cl.EnsureAuthenticated(force=False, refresh=True)
3176
3177 def fetch(cl):
3178 try:
3179 return (cl, cl.GetStatus())
3180 except:
3181 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003182 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003183 raise
3184
3185 threads_count = len(changes)
3186 if max_processes:
3187 threads_count = max(1, min(threads_count, max_processes))
3188 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3189
Edward Lemur61bf4172020-02-24 23:22:37 +00003190 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003191 fetched_cls = set()
3192 try:
3193 it = pool.imap_unordered(fetch, changes).__iter__()
3194 while True:
3195 try:
3196 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003197 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003198 break
3199 fetched_cls.add(cl)
3200 yield cl, status
3201 finally:
3202 pool.close()
3203
3204 # Add any branches that failed to fetch.
3205 for cl in set(changes) - fetched_cls:
3206 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003207
rmistry@google.com2dd99862015-06-22 12:22:18 +00003208
Jose Lopes3863fc52020-04-07 17:00:25 +00003209def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003210 """Uploads CLs of local branches that are dependents of the current branch.
3211
3212 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003213
3214 test1 -> test2.1 -> test3.1
3215 -> test3.2
3216 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003217
3218 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3219 run on the dependent branches in this order:
3220 test2.1, test3.1, test3.2, test2.2, test3.3
3221
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003222 Note: This function does not rebase your local dependent branches. Use it
3223 when you make a change to the parent branch that will not conflict
3224 with its dependent branches, and you would like their dependencies
3225 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003226 """
3227 if git_common.is_dirty_git_tree('upload-branch-deps'):
3228 return 1
3229
3230 root_branch = cl.GetBranch()
3231 if root_branch is None:
3232 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3233 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003234 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003235 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3236 'patchset dependencies without an uploaded CL.')
3237
3238 branches = RunGit(['for-each-ref',
3239 '--format=%(refname:short) %(upstream:short)',
3240 'refs/heads'])
3241 if not branches:
3242 print('No local branches found.')
3243 return 0
3244
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003245 # Create a dictionary of all local branches to the branches that are
3246 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003247 tracked_to_dependents = collections.defaultdict(list)
3248 for b in branches.splitlines():
3249 tokens = b.split()
3250 if len(tokens) == 2:
3251 branch_name, tracked = tokens
3252 tracked_to_dependents[tracked].append(branch_name)
3253
vapiera7fbd5a2016-06-16 09:17:49 -07003254 print()
3255 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003256 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003257
rmistry@google.com2dd99862015-06-22 12:22:18 +00003258 def traverse_dependents_preorder(branch, padding=''):
3259 dependents_to_process = tracked_to_dependents.get(branch, [])
3260 padding += ' '
3261 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003262 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003263 dependents.append(dependent)
3264 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003265
rmistry@google.com2dd99862015-06-22 12:22:18 +00003266 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003267 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003268
3269 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003270 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003271 return 0
3272
Jose Lopes3863fc52020-04-07 17:00:25 +00003273 if not force:
3274 confirm_or_exit('This command will checkout all dependent branches and run '
3275 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003276
rmistry@google.com2dd99862015-06-22 12:22:18 +00003277 # Record all dependents that failed to upload.
3278 failures = {}
3279 # Go through all dependents, checkout the branch and upload.
3280 try:
3281 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003282 print()
3283 print('--------------------------------------')
3284 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003285 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003286 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003287 try:
3288 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003289 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003290 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003291 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003292 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003293 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003294 finally:
3295 # Swap back to the original root branch.
3296 RunGit(['checkout', '-q', root_branch])
3297
vapiera7fbd5a2016-06-16 09:17:49 -07003298 print()
3299 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003300 for dependent_branch in dependents:
3301 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003302 print(' %s : %s' % (dependent_branch, upload_status))
3303 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003304
3305 return 0
3306
3307
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003308def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003309 """Given a proposed tag name, returns a tag name that is guaranteed to be
3310 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3311 or 'foo-3', and so on."""
3312
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003313 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003314 for suffix_num in itertools.count(1):
3315 if suffix_num == 1:
3316 to_check = proposed_tag
3317 else:
3318 to_check = '%s-%d' % (proposed_tag, suffix_num)
3319
3320 if to_check not in existing_tags:
3321 return to_check
3322
3323
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003324@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003325def CMDarchive(parser, args):
3326 """Archives and deletes branches associated with closed changelists."""
3327 parser.add_option(
3328 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003329 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003330 parser.add_option(
3331 '-f', '--force', action='store_true',
3332 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003333 parser.add_option(
3334 '-d', '--dry-run', action='store_true',
3335 help='Skip the branch tagging and removal steps.')
3336 parser.add_option(
3337 '-t', '--notags', action='store_true',
3338 help='Do not tag archived branches. '
3339 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003340 parser.add_option(
3341 '-p',
3342 '--pattern',
3343 default='git-cl-archived-{issue}-{branch}',
3344 help='Format string for archive tags. '
3345 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003346
kmarshall3bff56b2016-06-06 18:31:47 -07003347 options, args = parser.parse_args(args)
3348 if args:
3349 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003350
3351 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3352 if not branches:
3353 return 0
3354
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003355 tags = RunGit(['for-each-ref', '--format=%(refname)',
3356 'refs/tags']).splitlines() or []
3357 tags = [t.split('/')[-1] for t in tags]
3358
vapiera7fbd5a2016-06-16 09:17:49 -07003359 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003360 changes = [Changelist(branchref=b)
3361 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003362 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3363 statuses = get_cl_statuses(changes,
3364 fine_grained=True,
3365 max_processes=options.maxjobs)
3366 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003367 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3368 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003369 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003370 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003371 proposal.sort()
3372
3373 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003374 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003375 return 0
3376
Edward Lemur85153282020-02-14 22:06:29 +00003377 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003378
vapiera7fbd5a2016-06-16 09:17:49 -07003379 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003380 if options.notags:
3381 for next_item in proposal:
3382 print(' ' + next_item[0])
3383 else:
3384 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3385 for next_item in proposal:
3386 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003387
kmarshall9249e012016-08-23 12:02:16 -07003388 # Quit now on precondition failure or if instructed by the user, either
3389 # via an interactive prompt or by command line flags.
3390 if options.dry_run:
3391 print('\nNo changes were made (dry run).\n')
3392 return 0
3393 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003394 print('You are currently on a branch \'%s\' which is associated with a '
3395 'closed codereview issue, so archive cannot proceed. Please '
3396 'checkout another branch and run this command again.' %
3397 current_branch)
3398 return 1
kmarshall9249e012016-08-23 12:02:16 -07003399 elif not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00003400 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07003401 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003402 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003403 return 1
3404
3405 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003406 if not options.notags:
3407 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003408
3409 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3410 # Clean up the tag if we failed to delete the branch.
3411 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003412
vapiera7fbd5a2016-06-16 09:17:49 -07003413 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003414
3415 return 0
3416
3417
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003418@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003419def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003420 """Show status of changelists.
3421
3422 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003423 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003424 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003425 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003426 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003427 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003428 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003429 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003430
3431 Also see 'git cl comments'.
3432 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003433 parser.add_option(
3434 '--no-branch-color',
3435 action='store_true',
3436 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003437 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003438 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003439 parser.add_option('-f', '--fast', action='store_true',
3440 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003441 parser.add_option(
3442 '-j', '--maxjobs', action='store', type=int,
3443 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003444 parser.add_option(
3445 '-i', '--issue', type=int,
3446 help='Operate on this issue instead of the current branch\'s implicit '
3447 'issue. Requires --field to be set.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003448 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003449 if args:
3450 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003451
iannuccie53c9352016-08-17 14:40:40 -07003452 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003453 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003455 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003456 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003457 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003458 if cl.GetIssue():
3459 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003460 elif options.field == 'id':
3461 issueid = cl.GetIssue()
3462 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003463 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003464 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003465 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003466 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003467 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003468 elif options.field == 'status':
3469 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003470 elif options.field == 'url':
3471 url = cl.GetIssueURL()
3472 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003473 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003474 return 0
3475
3476 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3477 if not branches:
3478 print('No local branch found.')
3479 return 0
3480
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003481 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003482 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003483 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003484 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003485 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003486 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003487 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003488
Edward Lemur85153282020-02-14 22:06:29 +00003489 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00003490
3491 def FormatBranchName(branch, colorize=False):
3492 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3493 an asterisk when it is the current branch."""
3494
3495 asterisk = ""
3496 color = Fore.RESET
3497 if branch == current_branch:
3498 asterisk = "* "
3499 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00003500 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00003501
3502 if colorize:
3503 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003504 return asterisk + branch_name
3505
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003506 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003507
3508 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003509 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3510 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003511 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003512 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003513 branch_statuses[c.GetBranch()] = status
3514 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00003515 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003516 if url and (not status or status == 'error'):
3517 # The issue probably doesn't exist anymore.
3518 url += ' (broken)'
3519
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003520 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003521 # Turn off bold as well as colors.
3522 END = '\033[0m'
3523 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003524 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003525 color = ''
3526 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003527 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003528
Alan Cuttera3be9a52019-03-04 18:50:33 +00003529 branch_display = FormatBranchName(branch)
3530 padding = ' ' * (alignment - len(branch_display))
3531 if not options.no_branch_color:
3532 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003533
Alan Cuttera3be9a52019-03-04 18:50:33 +00003534 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3535 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003536
vapiera7fbd5a2016-06-16 09:17:49 -07003537 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003538 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003539 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003540 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003541 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003542 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003543 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003544 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003546 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003547 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003548 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 return 0
3550
3551
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003552def colorize_CMDstatus_doc():
3553 """To be called once in main() to add colors to git cl status help."""
3554 colors = [i for i in dir(Fore) if i[0].isupper()]
3555
3556 def colorize_line(line):
3557 for color in colors:
3558 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003559 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003560 indent = len(line) - len(line.lstrip(' ')) + 1
3561 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3562 return line
3563
3564 lines = CMDstatus.__doc__.splitlines()
3565 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3566
3567
phajdan.jre328cf92016-08-22 04:12:17 -07003568def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003569 if path == '-':
3570 json.dump(contents, sys.stdout)
3571 else:
3572 with open(path, 'w') as f:
3573 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003574
3575
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003576@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003577@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003579 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003580
3581 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003582 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003583 parser.add_option('-r', '--reverse', action='store_true',
3584 help='Lookup the branch(es) for the specified issues. If '
3585 'no issues are specified, all branches with mapped '
3586 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003587 parser.add_option('--json',
3588 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003589 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590
dnj@chromium.org406c4402015-03-03 17:22:28 +00003591 if options.reverse:
3592 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003593 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003594 # Reverse issue lookup.
3595 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003596
3597 git_config = {}
3598 for config in RunGit(['config', '--get-regexp',
3599 r'branch\..*issue']).splitlines():
3600 name, _space, val = config.partition(' ')
3601 git_config[name] = val
3602
dnj@chromium.org406c4402015-03-03 17:22:28 +00003603 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00003604 issue = git_config.get(
3605 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00003606 if issue:
3607 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003608 if not args:
Carlos Caballero81923d62020-07-06 18:22:27 +00003609 args = sorted(issue_branch_map.keys())
phajdan.jre328cf92016-08-22 04:12:17 -07003610 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003611 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003612 try:
3613 issue_num = int(issue)
3614 except ValueError:
3615 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003616 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003617 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003618 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003619 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003620 if options.json:
3621 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003622 return 0
3623
3624 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003625 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003626 if not issue.valid:
3627 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3628 'or no argument to list it.\n'
3629 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003630 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003631 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003632 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003633 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003634 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
3635 if options.json:
3636 write_json(options.json, {
3637 'issue': cl.GetIssue(),
3638 'issue_url': cl.GetIssueURL(),
3639 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640 return 0
3641
3642
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003643@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003644def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003645 """Shows or posts review comments for any changelist."""
3646 parser.add_option('-a', '--add-comment', dest='comment',
3647 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003648 parser.add_option('-p', '--publish', action='store_true',
3649 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01003650 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00003651 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003652 parser.add_option('-m', '--machine-readable', dest='readable',
3653 action='store_false', default=True,
3654 help='output comments in a format compatible with '
3655 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00003656 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07003657 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003658 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003659
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003660 issue = None
3661 if options.issue:
3662 try:
3663 issue = int(options.issue)
3664 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003665 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003666
Edward Lemur934836a2019-09-09 20:16:54 +00003667 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003668
3669 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003670 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003671 return 0
3672
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003673 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
3674 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003675 for comment in summary:
3676 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003677 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003678 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003679 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003680 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003681 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00003682 elif comment.autogenerated:
3683 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003684 else:
3685 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003686 print('\n%s%s %s%s\n%s' % (
3687 color,
3688 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
3689 comment.sender,
3690 Fore.RESET,
3691 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
3692
smut@google.comc85ac942015-09-15 16:34:43 +00003693 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003694 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00003695 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003696 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
3697 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00003698 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003699 return 0
3700
3701
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003702@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003703@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003704def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003705 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003706 parser.add_option('-d', '--display', action='store_true',
3707 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003708 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003709 help='New description to set for this issue (- for stdin, '
3710 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003711 parser.add_option('-f', '--force', action='store_true',
3712 help='Delete any unpublished Gerrit edits for this issue '
3713 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003714
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003715 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003716
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003717 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003718 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003719 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003720 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003721 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003722
Edward Lemur934836a2019-09-09 20:16:54 +00003723 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003724 if target_issue_arg:
3725 kwargs['issue'] = target_issue_arg.issue
3726 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003727
3728 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003729 if not cl.GetIssue():
3730 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003731
Edward Lemur678a6842019-10-03 22:25:05 +00003732 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00003733 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003734
Edward Lemur6c6827c2020-02-06 21:15:18 +00003735 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003736
smut@google.com34fb6b12015-07-13 20:03:26 +00003737 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003738 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003739 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003740
3741 if options.new_description:
3742 text = options.new_description
3743 if text == '-':
3744 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003745 elif text == '+':
3746 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00003747 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003748
3749 description.set_description(text)
3750 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003751 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00003752 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003753 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003754 return 0
3755
3756
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003757@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003758def CMDlint(parser, args):
3759 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003760 parser.add_option('--filter', action='append', metavar='-x,+y',
3761 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003762 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003763
3764 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003765 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00003766 try:
3767 import cpplint
3768 import cpplint_chromium
3769 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003770 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003771 return 1
3772
3773 # Change the current working directory before calling lint so that it
3774 # shows the correct base.
3775 previous_cwd = os.getcwd()
3776 os.chdir(settings.GetRoot())
3777 try:
Edward Lemur934836a2019-09-09 20:16:54 +00003778 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00003779 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003780 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003781 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003782 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003783
Lei Zhangb8c62cf2020-07-15 20:09:37 +00003784 # Process cpplint arguments, if any.
3785 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
3786 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003787 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003788
Lei Zhang379d1ad2020-07-15 19:40:06 +00003789 include_regex = re.compile(settings.GetLintRegex())
3790 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00003791 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3792 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00003793 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003794 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00003795 continue
3796
3797 if ignore_regex.match(filename):
3798 print('Ignoring file %s' % filename)
3799 continue
3800
3801 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3802 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003803 finally:
3804 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003805 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003806 if cpplint._cpplint_state.error_count != 0:
3807 return 1
3808 return 0
3809
3810
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003811@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003812def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003813 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003814 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003815 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003816 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003817 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08003818 parser.add_option('--all', action='store_true',
3819 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04003820 parser.add_option('--parallel', action='store_true',
3821 help='Run all tests specified by input_api.RunTests in all '
3822 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003823 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824
sbc@chromium.org71437c02015-04-09 19:29:40 +00003825 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003827 return 1
3828
Edward Lemur934836a2019-09-09 20:16:54 +00003829 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 if args:
3831 base_branch = args[0]
3832 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003833 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003834 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003835
Gregory Nisbet29d5cf82020-02-27 08:16:58 +00003836 if cl.GetIssue():
3837 description = cl.FetchDescription()
Aaron Gable8076c282017-11-29 14:39:41 -08003838 else:
Edward Lemura12175c2020-03-09 16:58:26 +00003839 description = _create_description_from_log([base_branch])
Aaron Gable8076c282017-11-29 14:39:41 -08003840
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003841 cl.RunHook(
3842 committing=not options.upload,
3843 may_prompt=False,
3844 verbose=options.verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00003845 parallel=options.parallel,
3846 upstream=base_branch,
3847 description=description,
3848 all_files=options.all)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003849 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003850
3851
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003852def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003853 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003854
3855 Works the same way as
3856 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3857 but can be called on demand on all platforms.
3858
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003859 The basic idea is to generate git hash of a state of the tree, original
3860 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003861 """
3862 lines = []
3863 tree_hash = RunGitSilent(['write-tree'])
3864 lines.append('tree %s' % tree_hash.strip())
3865 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3866 if code == 0:
3867 lines.append('parent %s' % parent.strip())
3868 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3869 lines.append('author %s' % author.strip())
3870 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3871 lines.append('committer %s' % committer.strip())
3872 lines.append('')
3873 # Note: Gerrit's commit-hook actually cleans message of some lines and
3874 # whitespace. This code is not doing this, but it clearly won't decrease
3875 # entropy.
3876 lines.append(message)
3877 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00003878 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003879 return 'I%s' % change_hash.strip()
3880
3881
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01003882def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00003883 """Computes the remote branch ref to use for the CL.
3884
3885 Args:
3886 remote (str): The git remote for the CL.
3887 remote_branch (str): The git remote branch for the CL.
3888 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00003889 """
3890 if not (remote and remote_branch):
3891 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003892
wittman@chromium.org455dc922015-01-26 20:15:50 +00003893 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003894 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00003895 # refs, which are then translated into the remote full symbolic refs
3896 # below.
3897 if '/' not in target_branch:
3898 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3899 else:
3900 prefix_replacements = (
3901 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3902 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3903 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3904 )
3905 match = None
3906 for regex, replacement in prefix_replacements:
3907 match = re.search(regex, target_branch)
3908 if match:
3909 remote_branch = target_branch.replace(match.group(0), replacement)
3910 break
3911 if not match:
3912 # This is a branch path but not one we recognize; use as-is.
3913 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003914 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3915 # Handle the refs that need to land in different refs.
3916 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003917
wittman@chromium.org455dc922015-01-26 20:15:50 +00003918 # Create the true path to the remote branch.
3919 # Does the following translation:
3920 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3921 # * refs/remotes/origin/master -> refs/heads/master
3922 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3923 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3924 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3925 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3926 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3927 'refs/heads/')
3928 elif remote_branch.startswith('refs/remotes/branch-heads'):
3929 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01003930
wittman@chromium.org455dc922015-01-26 20:15:50 +00003931 return remote_branch
3932
3933
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003934def cleanup_list(l):
3935 """Fixes a list so that comma separated items are put as individual items.
3936
3937 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3938 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3939 """
3940 items = sum((i.split(',') for i in l), [])
3941 stripped_items = (i.strip() for i in items)
3942 return sorted(filter(None, stripped_items))
3943
3944
Aaron Gable4db38df2017-11-03 14:59:07 -07003945@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003946@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003947def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003948 """Uploads the current changelist to codereview.
3949
3950 Can skip dependency patchset uploads for a branch by running:
3951 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003952 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00003953 git config --unset branch.branch_name.skip-deps-uploads
3954 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02003955
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003956 If the name of the checked out branch starts with "bug-" or "fix-" followed
3957 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02003958 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003959
3960 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003961 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003962 [git-cl] add support for hashtags
3963 Foo bar: implement foo
3964 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00003965 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003966 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3967 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003968 parser.add_option('--bypass-watchlists', action='store_true',
3969 dest='bypass_watchlists',
3970 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07003971 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003972 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003973 parser.add_option('--message', '-m', dest='message',
3974 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003975 parser.add_option('-b', '--bug',
3976 help='pre-populate the bug number(s) for this issue. '
3977 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003978 parser.add_option('--message-file', dest='message_file',
3979 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003980 parser.add_option('--title', '-t', dest='title',
3981 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003982 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003983 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003984 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003985 parser.add_option('--tbrs',
3986 action='append', default=[],
3987 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003988 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003989 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003990 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003991 parser.add_option('--hashtag', dest='hashtags',
3992 action='append', default=[],
3993 help=('Gerrit hashtag for new CL; '
3994 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00003995 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08003996 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003997 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003998 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003999 metavar='TARGET',
4000 help='Apply CL to remote ref TARGET. ' +
4001 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004002 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004003 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004004 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004005 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004006 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004007 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004008 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4009 const='TBR', help='add a set of OWNERS to TBR')
4010 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4011 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004012 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004013 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004014 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004015 'implies --send-mail')
4016 parser.add_option('-d', '--cq-dry-run',
4017 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004018 help='Send the patchset to do a CQ dry run right after '
4019 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004020 parser.add_option('--preserve-tryjobs', action='store_true',
4021 help='instruct the CQ to let tryjobs running even after '
4022 'new patchsets are uploaded instead of canceling '
4023 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004024 parser.add_option('--dependencies', action='store_true',
4025 help='Uploads CLs of all the local branches that depend on '
4026 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004027 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4028 help='Sends your change to the CQ after an approval. Only '
4029 'works on repos that have the Auto-Submit label '
4030 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004031 parser.add_option('--parallel', action='store_true',
4032 help='Run all tests specified by input_api.RunTests in all '
4033 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004034 parser.add_option('--no-autocc', action='store_true',
4035 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004036 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004037 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004038 parser.add_option('-R', '--retry-failed', action='store_true',
4039 help='Retry failed tryjobs from old patchset immediately '
4040 'after uploading new patchset. Cannot be used with '
4041 '--use-commit-queue or --cq-dry-run.')
4042 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4043 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004044 parser.add_option('--fixed', '-x',
4045 help='List of bugs that will be commented on and marked '
4046 'fixed (pre-populates "Fixed:" tag). Same format as '
4047 '-b option / "Bug:" tag. If fixing several issues, '
4048 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004049 parser.add_option('--edit-description', action='store_true', default=False,
4050 help='Modify description before upload. Cannot be used '
4051 'with --force. It is a noop when --no-squash is set '
4052 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004053 parser.add_option('--git-completion-helper', action="store_true",
4054 help=optparse.SUPPRESS_HELP)
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004055
rmistry@google.com2dd99862015-06-22 12:22:18 +00004056 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004057 (options, args) = parser.parse_args(args)
4058
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004059 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004060 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4061 if opt.help != optparse.SUPPRESS_HELP))
4062 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004063
sbc@chromium.org71437c02015-04-09 19:29:40 +00004064 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004065 return 1
4066
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004067 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004068 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004069 options.cc = cleanup_list(options.cc)
4070
Josipe827b0f2020-01-30 00:07:20 +00004071 if options.edit_description and options.force:
4072 parser.error('Only one of --force and --edit-description allowed')
4073
tandriib80458a2016-06-23 12:20:07 -07004074 if options.message_file:
4075 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004076 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004077 options.message = gclient_utils.FileRead(options.message_file)
4078 options.message_file = None
4079
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004080 if ([options.cq_dry_run,
4081 options.use_commit_queue,
4082 options.retry_failed].count(True) > 1):
4083 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4084 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004085
Aaron Gableedbc4132017-09-11 13:22:28 -07004086 if options.use_commit_queue:
4087 options.send_mail = True
4088
Edward Lesmes0dd54822020-03-26 18:24:25 +00004089 if options.squash is None:
4090 # Load default for user, repo, squash=true, in this order.
4091 options.squash = settings.GetSquashGerritUploads()
4092
Edward Lemur934836a2019-09-09 20:16:54 +00004093 cl = Changelist()
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004094 # Warm change details cache now to avoid RPCs later, reducing latency for
4095 # developers.
4096 if cl.GetIssue():
4097 cl._GetChangeDetail(
4098 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4099
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004100 if options.retry_failed and not cl.GetIssue():
4101 print('No previous patchsets, so --retry-failed has no effect.')
4102 options.retry_failed = False
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004103
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004104 # cl.GetMostRecentPatchset uses cached information, and can return the last
4105 # patchset before upload. Calling it here makes it clear that it's the
4106 # last patchset before upload. Note that GetMostRecentPatchset will fail
4107 # if no CL has been uploaded yet.
4108 if options.retry_failed:
4109 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004110
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004111 ret = cl.CMDUpload(options, args, orig_args)
4112
4113 if options.retry_failed:
4114 if ret != 0:
4115 print('Upload failed, so --retry-failed has no effect.')
4116 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004117 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004118 cl, options.buildbucket_host, latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004119 jobs = _filter_failed_for_retry(builds)
4120 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004121 print('No failed tryjobs, so --retry-failed has no effect.')
4122 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004123 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004124
4125 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004126
4127
Francois Dorayd42c6812017-05-30 15:10:20 -04004128@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004129@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004130def CMDsplit(parser, args):
4131 """Splits a branch into smaller branches and uploads CLs.
4132
4133 Creates a branch and uploads a CL for each group of files modified in the
4134 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00004135 comment, the string '$directory', is replaced with the directory containing
4136 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04004137 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004138 parser.add_option('-d', '--description', dest='description_file',
4139 help='A text file containing a CL description in which '
4140 '$directory will be replaced by each CL\'s directory.')
4141 parser.add_option('-c', '--comment', dest='comment_file',
4142 help='A text file containing a CL comment.')
4143 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004144 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004145 help='List the files and reviewers for each CL that would '
4146 'be created, but don\'t create branches or CLs.')
4147 parser.add_option('--cq-dry-run', action='store_true',
4148 help='If set, will do a cq dry run for each uploaded CL. '
4149 'Please be careful when doing this; more than ~10 CLs '
4150 'has the potential to overload our build '
4151 'infrastructure. Try to upload these not during high '
4152 'load times (usually 11-3 Mountain View time). Email '
4153 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004154 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4155 default=True,
4156 help='Sends your change to the CQ after an approval. Only '
4157 'works on repos that have the Auto-Submit label '
4158 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004159 options, _ = parser.parse_args(args)
4160
4161 if not options.description_file:
4162 parser.error('No --description flag specified.')
4163
4164 def WrappedCMDupload(args):
4165 return CMDupload(OptionParser(), args)
4166
Edward Lemur2c62b332020-03-12 22:12:33 +00004167 return split_cl.SplitCl(
4168 options.description_file, options.comment_file, Changelist,
4169 WrappedCMDupload, options.dry_run, options.cq_dry_run,
4170 options.enable_auto_submit, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04004171
4172
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004173@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004174@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004175def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004176 """DEPRECATED: Used to commit the current changelist via git-svn."""
4177 message = ('git-cl no longer supports committing to SVN repositories via '
4178 'git-svn. You probably want to use `git cl land` instead.')
4179 print(message)
4180 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004181
4182
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004183@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004184@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004185def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004186 """Commits the current changelist via git.
4187
4188 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4189 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004190 """
4191 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4192 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004193 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004194 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004195 parser.add_option('--parallel', action='store_true',
4196 help='Run all tests specified by input_api.RunTests in all '
4197 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004198 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004199
Edward Lemur934836a2019-09-09 20:16:54 +00004200 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004201
Robert Iannucci2e73d432018-03-14 01:10:47 -07004202 if not cl.GetIssue():
4203 DieWithError('You must upload the change first to Gerrit.\n'
4204 ' If you would rather have `git cl land` upload '
4205 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004206 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004207 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208
4209
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004210@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004211@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004213 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004214 parser.add_option('-b', dest='newbranch',
4215 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004216 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004217 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004219 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004220
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004221 group = optparse.OptionGroup(
4222 parser,
4223 'Options for continuing work on the current issue uploaded from a '
4224 'different clone (e.g. different machine). Must be used independently '
4225 'from the other options. No issue number should be specified, and the '
4226 'branch must have an issue number associated with it')
4227 group.add_option('--reapply', action='store_true', dest='reapply',
4228 help='Reset the branch and reapply the issue.\n'
4229 'CAUTION: This will undo any local changes in this '
4230 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004231
4232 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004233 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004234 parser.add_option_group(group)
4235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004236 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004238 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004239 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004240 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004241 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004242 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004243
Edward Lemur934836a2019-09-09 20:16:54 +00004244 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004245 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004246 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004247
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004248 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004249 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004250 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004251
4252 RunGit(['reset', '--hard', upstream])
4253 if options.pull:
4254 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004255
Edward Lemur678a6842019-10-03 22:25:05 +00004256 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4257 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004258
4259 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004260 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004261
Edward Lemurf38bc172019-09-03 21:02:13 +00004262 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004263 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004264 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004265
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004266 # We don't want uncommitted changes mixed up with the patch.
4267 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004268 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004270 if options.newbranch:
4271 if options.force:
4272 RunGit(['branch', '-D', options.newbranch],
4273 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00004274 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004275
Edward Lemur678a6842019-10-03 22:25:05 +00004276 cl = Changelist(
4277 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004278
Edward Lemur678a6842019-10-03 22:25:05 +00004279 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004280 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004281
Edward Lemurf38bc172019-09-03 21:02:13 +00004282 return cl.CMDPatchWithParsedIssue(
4283 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004284
4285
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004286def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287 """Fetches the tree status and returns either 'open', 'closed',
4288 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004289 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004290 if url:
Edward Lemur79d4f992019-11-11 23:49:02 +00004291 status = urllib.request.urlopen(url).read().lower()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004292 if status.find('closed') != -1 or status == '0':
4293 return 'closed'
4294 elif status.find('open') != -1 or status == '1':
4295 return 'open'
4296 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004297 return 'unset'
4298
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004299
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300def GetTreeStatusReason():
4301 """Fetches the tree status from a json url and returns the message
4302 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004303 url = settings.GetTreeStatusUrl()
4304 json_url = urlparse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004305 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004306 status = json.loads(connection.read())
4307 connection.close()
4308 return status['message']
4309
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004310
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004311@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004312def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004313 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004314 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315 status = GetTreeStatus()
4316 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 return 2
4319
vapiera7fbd5a2016-06-16 09:17:49 -07004320 print('The tree is %s' % status)
4321 print()
4322 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004323 if status != 'open':
4324 return 1
4325 return 0
4326
4327
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004328@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004329def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004330 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4331 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004332 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004333 '-b', '--bot', action='append',
4334 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4335 'times to specify multiple builders. ex: '
4336 '"-b win_rel -b win_layout". See '
4337 'the try server waterfall for the builders name and the tests '
4338 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004339 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004340 '-B', '--bucket', default='',
4341 help=('Buildbucket bucket to send the try requests.'))
4342 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004343 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004344 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004345 'be determined by the try recipe that builder runs, which usually '
4346 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004347 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004348 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004349 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004350 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004351 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004352 '--category', default='git_cl_try', help='Specify custom build category.')
4353 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004354 '--project',
4355 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004356 'in recipe to determine to which repository or directory to '
4357 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004358 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004359 '-p', '--property', dest='properties', action='append', default=[],
4360 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004361 'key2=value2 etc. The value will be treated as '
4362 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004363 'NOTE: using this may make your tryjob not usable for CQ, '
4364 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004365 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004366 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4367 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004368 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004369 parser.add_option(
4370 '-R', '--retry-failed', action='store_true', default=False,
4371 help='Retry failed jobs from the latest set of tryjobs. '
4372 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004373 parser.add_option(
4374 '-i', '--issue', type=int,
4375 help='Operate on this issue instead of the current branch\'s implicit '
4376 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004377 options, args = parser.parse_args(args)
4378
machenbach@chromium.org45453142015-09-15 08:45:22 +00004379 # Make sure that all properties are prop=value pairs.
4380 bad_params = [x for x in options.properties if '=' not in x]
4381 if bad_params:
4382 parser.error('Got properties with missing "=": %s' % bad_params)
4383
maruel@chromium.org15192402012-09-06 12:38:29 +00004384 if args:
4385 parser.error('Unknown arguments: %s' % args)
4386
Edward Lemur934836a2019-09-09 20:16:54 +00004387 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004388 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004389 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004390
Edward Lemurf38bc172019-09-03 21:02:13 +00004391 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004392 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004393
tandriie113dfd2016-10-11 10:20:12 -07004394 error_message = cl.CannotTriggerTryJobReason()
4395 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004396 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004397
Edward Lemur45768512020-03-02 19:03:14 +00004398 if options.bot:
4399 if options.retry_failed:
4400 parser.error('--bot is not compatible with --retry-failed.')
4401 if not options.bucket:
4402 parser.error('A bucket (e.g. "chromium/try") is required.')
4403
4404 triggered = [b for b in options.bot if 'triggered' in b]
4405 if triggered:
4406 parser.error(
4407 'Cannot schedule builds on triggered bots: %s.\n'
4408 'This type of bot requires an initial job from a parent (usually a '
4409 'builder). Schedule a job on the parent instead.\n' % triggered)
4410
4411 if options.bucket.startswith('.master'):
4412 parser.error('Buildbot masters are not supported.')
4413
4414 project, bucket = _parse_bucket(options.bucket)
4415 if project is None or bucket is None:
4416 parser.error('Invalid bucket: %s.' % options.bucket)
4417 jobs = sorted((project, bucket, bot) for bot in options.bot)
4418 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004419 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004420 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004421 if options.verbose:
4422 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00004423 jobs = _filter_failed_for_retry(builds)
4424 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004425 print('There are no failed jobs in the latest set of jobs '
4426 '(patchset #%d), doing nothing.' % patchset)
4427 return 0
Edward Lemur45768512020-03-02 19:03:14 +00004428 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004429 if num_builders > 10:
4430 confirm_or_exit('There are %d builders with failed builds.'
4431 % num_builders, action='continue')
4432 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07004433 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004434 print('git cl try with no bots now defaults to CQ dry run.')
4435 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4436 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004437
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004438 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004439 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004440 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004441 except BuildbucketResponseException as ex:
4442 print('ERROR: %s' % ex)
4443 return 1
4444 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004445
4446
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004447@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004448def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004449 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004450 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004451 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004452 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004453 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004454 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004455 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004456 '--color', action='store_true', default=setup_color.IS_TTY,
4457 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004458 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004459 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4460 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004461 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004462 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004463 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004464 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00004465 parser.add_option(
4466 '-i', '--issue', type=int,
4467 help='Operate on this issue instead of the current branch\'s implicit '
4468 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004469 options, args = parser.parse_args(args)
4470 if args:
4471 parser.error('Unrecognized args: %s' % ' '.join(args))
4472
Edward Lemur934836a2019-09-09 20:16:54 +00004473 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004474 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004475 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004476
tandrii221ab252016-10-06 08:12:04 -07004477 patchset = options.patchset
4478 if not patchset:
4479 patchset = cl.GetMostRecentPatchset()
4480 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004481 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004482 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004483 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004484 cl.GetIssue())
4485
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004486 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004487 jobs = _fetch_tryjobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004488 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004489 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004490 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004491 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004492 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004493 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004494 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004495 return 0
4496
4497
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004498@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004499@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004500def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004501 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004502 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004503 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004504 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004505
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004506 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004507 if args:
4508 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004509 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004510 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004511 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004512 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004513
4514 # Clear configured merge-base, if there is one.
4515 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004516 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004517 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518 return 0
4519
4520
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004521@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004522def CMDweb(parser, args):
4523 """Opens the current CL in the web browser."""
4524 _, args = parser.parse_args(args)
4525 if args:
4526 parser.error('Unrecognized args: %s' % ' '.join(args))
4527
4528 issue_url = Changelist().GetIssueURL()
4529 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004530 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004531 return 1
4532
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004533 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004534 # allows us to hide the "Created new window in existing browser session."
4535 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004536 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004537 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004538 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004539 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004540 os.open(os.devnull, os.O_RDWR)
4541 try:
4542 webbrowser.open(issue_url)
4543 finally:
4544 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004545 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004546 return 0
4547
4548
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004549@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004550def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004551 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004552 parser.add_option('-d', '--dry-run', action='store_true',
4553 help='trigger in dry run mode')
4554 parser.add_option('-c', '--clear', action='store_true',
4555 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00004556 parser.add_option(
4557 '-i', '--issue', type=int,
4558 help='Operate on this issue instead of the current branch\'s implicit '
4559 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004560 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004561 if args:
4562 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004563 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004564 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004565
Edward Lemur934836a2019-09-09 20:16:54 +00004566 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004567 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004568 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004569 elif options.dry_run:
4570 state = _CQState.DRY_RUN
4571 else:
4572 state = _CQState.COMMIT
4573 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004574 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004575 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004576 return 0
4577
4578
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004579@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004580def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004581 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00004582 parser.add_option(
4583 '-i', '--issue', type=int,
4584 help='Operate on this issue instead of the current branch\'s implicit '
4585 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004586 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004587 if args:
4588 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004589 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004590 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004591 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004592 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004593 cl.CloseIssue()
4594 return 0
4595
4596
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004597@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004598def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004599 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004600 parser.add_option(
4601 '--stat',
4602 action='store_true',
4603 dest='stat',
4604 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004605 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004606 if args:
4607 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004608
Edward Lemur934836a2019-09-09 20:16:54 +00004609 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004610 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004611 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004612 if not issue:
4613 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004614
Aaron Gablea718c3e2017-08-28 17:47:28 -07004615 base = cl._GitGetBranchConfigValue('last-upload-hash')
4616 if not base:
4617 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4618 if not base:
4619 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4620 revision_info = detail['revisions'][detail['current_revision']]
4621 fetch_info = revision_info['fetch']['http']
4622 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4623 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004624
Aaron Gablea718c3e2017-08-28 17:47:28 -07004625 cmd = ['git', 'diff']
4626 if options.stat:
4627 cmd.append('--stat')
4628 cmd.append(base)
4629 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004630
4631 return 0
4632
4633
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004634@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004635def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004636 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004637 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004638 '--ignore-current',
4639 action='store_true',
4640 help='Ignore the CL\'s current reviewers and start from scratch.')
4641 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004642 '--ignore-self',
4643 action='store_true',
4644 help='Do not consider CL\'s author as an owners.')
4645 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004646 '--no-color',
4647 action='store_true',
4648 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07004649 parser.add_option(
4650 '--batch',
4651 action='store_true',
4652 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00004653 # TODO: Consider moving this to another command, since other
4654 # git-cl owners commands deal with owners for a given CL.
4655 parser.add_option(
4656 '--show-all',
4657 action='store_true',
4658 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004659 options, args = parser.parse_args(args)
4660
Edward Lemur934836a2019-09-09 20:16:54 +00004661 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00004662 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004663
Yang Guo6e269a02019-06-26 11:17:02 +00004664 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00004665 if len(args) == 0:
4666 print('No files specified for --show-all. Nothing to do.')
4667 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00004668 for arg in args:
4669 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemurb7f759f2020-03-04 21:20:56 +00004670 database = owners.Database(settings.GetRoot(), open, os.path)
Yang Guo6e269a02019-06-26 11:17:02 +00004671 database.load_data_needed_for([arg])
4672 print('Owners for %s:' % arg)
4673 for owner in sorted(database.all_possible_owners([arg], None)):
4674 print(' - %s' % owner)
4675 return 0
4676
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004677 if args:
4678 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004679 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004680 base_branch = args[0]
4681 else:
4682 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004683 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004684
Edward Lemur2c62b332020-03-12 22:12:33 +00004685 root = settings.GetRoot()
4686 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07004687
4688 if options.batch:
Edward Lemur2c62b332020-03-12 22:12:33 +00004689 db = owners.Database(root, open, os.path)
Dirk Prankebf980882017-09-02 15:08:00 -07004690 print('\n'.join(db.reviewers_for(affected_files, author)))
4691 return 0
4692
Edward Lemur2c62b332020-03-12 22:12:33 +00004693 owner_files = [f for f in affected_files if 'OWNERS' in os.path.basename(f)]
4694 original_owner_files = {
4695 f: scm.GIT.GetOldContents(root, f, base_branch).splitlines()
4696 for f in owner_files}
4697
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004698 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07004699 affected_files,
Edward Lemur2c62b332020-03-12 22:12:33 +00004700 root,
Edward Lemur707d70b2018-02-07 00:50:14 +01004701 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004702 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur2c62b332020-03-12 22:12:33 +00004703 fopen=open,
4704 os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02004705 disable_color=options.no_color,
Edward Lemur2c62b332020-03-12 22:12:33 +00004706 override_files=original_owner_files,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004707 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004708
4709
Aiden Bennerc08566e2018-10-03 17:52:42 +00004710def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004711 """Generates a diff command."""
4712 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00004713 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
4714
Aiden Benner6c18a1a2018-11-23 20:18:23 +00004715 if allow_prefix:
4716 # explicitly setting --src-prefix and --dst-prefix is necessary in the
4717 # case that diff.noprefix is set in the user's git config.
4718 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
4719 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00004720 diff_cmd += ['--no-prefix']
4721
4722 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004723
4724 if args:
4725 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004726 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004727 diff_cmd.append(arg)
4728 else:
4729 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004730
4731 return diff_cmd
4732
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004733
Jamie Madill5e96ad12020-01-13 16:08:35 +00004734def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
4735 """Runs clang-format-diff and sets a return value if necessary."""
4736
4737 if not clang_diff_files:
4738 return 0
4739
4740 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4741 # formatted. This is used to block during the presubmit.
4742 return_value = 0
4743
4744 # Locate the clang-format binary in the checkout
4745 try:
4746 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4747 except clang_format.NotFoundError as e:
4748 DieWithError(e)
4749
4750 if opts.full or settings.GetFormatFullByDefault():
4751 cmd = [clang_format_tool]
4752 if not opts.dry_run and not opts.diff:
4753 cmd.append('-i')
4754 if opts.dry_run:
4755 for diff_file in clang_diff_files:
4756 with open(diff_file, 'r') as myfile:
4757 code = myfile.read().replace('\r\n', '\n')
4758 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
4759 stdout = stdout.replace('\r\n', '\n')
4760 if opts.diff:
4761 sys.stdout.write(stdout)
4762 if code != stdout:
4763 return_value = 2
4764 else:
4765 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
4766 if opts.diff:
4767 sys.stdout.write(stdout)
4768 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00004769 try:
4770 script = clang_format.FindClangFormatScriptInChromiumTree(
4771 'clang-format-diff.py')
4772 except clang_format.NotFoundError as e:
4773 DieWithError(e)
4774
Edward Lesmes89624cd2020-04-06 17:51:56 +00004775 cmd = ['vpython', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00004776 if not opts.dry_run and not opts.diff:
4777 cmd.append('-i')
4778
4779 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00004780 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00004781
Edward Lesmes89624cd2020-04-06 17:51:56 +00004782 env = os.environ.copy()
4783 env['PATH'] = (
4784 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
4785 stdout = RunCommand(
4786 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00004787 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00004788 if opts.diff:
4789 sys.stdout.write(stdout)
4790 if opts.dry_run and len(stdout) > 0:
4791 return_value = 2
4792
4793 return return_value
4794
4795
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004796def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004797 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004798 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004799
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004800
enne@chromium.org555cfe42014-01-29 18:21:39 +00004801@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004802@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004803def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004804 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11004805 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004806 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004807 parser.add_option('--full', action='store_true',
4808 help='Reformat the full content of all touched files')
4809 parser.add_option('--dry-run', action='store_true',
4810 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004811 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004812 '--no-clang-format',
4813 dest='clang_format',
4814 action='store_false',
4815 default=True,
4816 help='Disables formatting of various file types using clang-format.')
4817 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004818 '--python',
4819 action='store_true',
4820 default=None,
4821 help='Enables python formatting on all python files.')
4822 parser.add_option(
4823 '--no-python',
4824 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00004825 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004826 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00004827 'If neither --python or --no-python are set, python files that have a '
4828 '.style.yapf file in an ancestor directory will be formatted. '
4829 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004830 parser.add_option(
4831 '--js',
4832 action='store_true',
4833 help='Format javascript code with clang-format. '
4834 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004835 parser.add_option('--diff', action='store_true',
4836 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07004837 parser.add_option('--presubmit', action='store_true',
4838 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004839 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004840
Garrett Beaty91a6f332020-01-06 16:57:24 +00004841 if opts.python is not None and opts.no_python:
4842 raise parser.error('Cannot set both --python and --no-python')
4843 if opts.no_python:
4844 opts.python = False
4845
Daniel Chengc55eecf2016-12-30 03:11:02 -08004846 # Normalize any remaining args against the current path, so paths relative to
4847 # the current directory are still resolved as expected.
4848 args = [os.path.join(os.getcwd(), arg) for arg in args]
4849
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004850 # git diff generates paths against the root of the repository. Change
4851 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004852 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004853 if rel_base_path:
4854 os.chdir(rel_base_path)
4855
digit@chromium.org29e47272013-05-17 17:01:46 +00004856 # Grab the merge-base commit, i.e. the upstream commit of the current
4857 # branch when it was created or the last time it was rebased. This is
4858 # to cover the case where the user may have called "git fetch origin",
4859 # moving the origin branch to a newer commit, but hasn't rebased yet.
4860 upstream_commit = None
4861 cl = Changelist()
4862 upstream_branch = cl.GetUpstreamBranch()
4863 if upstream_branch:
4864 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4865 upstream_commit = upstream_commit.strip()
4866
4867 if not upstream_commit:
4868 DieWithError('Could not find base commit for this branch. '
4869 'Are you in detached state?')
4870
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004871 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4872 diff_output = RunGit(changed_files_cmd)
4873 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004874 # Filter out files deleted by this CL
4875 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004876
Andreas Haas417d89c2020-02-06 10:24:27 +00004877 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00004878 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11004879
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004880 clang_diff_files = []
4881 if opts.clang_format:
4882 clang_diff_files = [
4883 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
4884 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004885 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004886 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004887
Edward Lesmes50da7702020-03-30 19:23:43 +00004888 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004889
Jamie Madill5e96ad12020-01-13 16:08:35 +00004890 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
4891 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004892
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004893 # Similar code to above, but using yapf on .py files rather than clang-format
4894 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004895 py_explicitly_disabled = opts.python is not None and not opts.python
4896 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00004897 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
4898 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004899
Aiden Bennerc08566e2018-10-03 17:52:42 +00004900 # Used for caching.
4901 yapf_configs = {}
4902 for f in python_diff_files:
4903 # Find the yapf style config for the current file, defaults to depot
4904 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004905 _FindYapfConfigFile(f, yapf_configs, top_dir)
4906
4907 # Turn on python formatting by default if a yapf config is specified.
4908 # This breaks in the case of this repo though since the specified
4909 # style file is also the global default.
4910 if opts.python is None:
4911 filtered_py_files = []
4912 for f in python_diff_files:
4913 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
4914 filtered_py_files.append(f)
4915 else:
4916 filtered_py_files = python_diff_files
4917
4918 # Note: yapf still seems to fix indentation of the entire file
4919 # even if line ranges are specified.
4920 # See https://github.com/google/yapf/issues/499
4921 if not opts.full and filtered_py_files:
4922 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
4923
Brian Sheedyb4307d52019-12-02 19:18:17 +00004924 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
4925 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
4926 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00004927
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004928 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00004929 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
4930 # Default to pep8 if not .style.yapf is found.
4931 if not yapf_style:
4932 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00004933
Peter Wend9399922020-06-17 17:33:49 +00004934 with open(f, 'r') as py_f:
4935 if 'python3' in py_f.readline():
4936 vpython_script = 'vpython3'
4937 else:
4938 vpython_script = 'vpython'
4939
4940 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00004941
4942 has_formattable_lines = False
4943 if not opts.full:
4944 # Only run yapf over changed line ranges.
4945 for diff_start, diff_len in py_line_diffs[f]:
4946 diff_end = diff_start + diff_len - 1
4947 # Yapf errors out if diff_end < diff_start but this
4948 # is a valid line range diff for a removal.
4949 if diff_end >= diff_start:
4950 has_formattable_lines = True
4951 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
4952 # If all line diffs were removals we have nothing to format.
4953 if not has_formattable_lines:
4954 continue
4955
4956 if opts.diff or opts.dry_run:
4957 cmd += ['--diff']
4958 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00004959 stdout = RunCommand(cmd,
4960 error_ok=True,
4961 cwd=top_dir,
4962 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00004963 if opts.diff:
4964 sys.stdout.write(stdout)
4965 elif len(stdout) > 0:
4966 return_value = 2
4967 else:
4968 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00004969 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004970
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004971 # Format GN build files. Always run on full build files for canonical form.
4972 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004973 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07004974 if opts.dry_run or opts.diff:
4975 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004976 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07004977 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00004978 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07004979 cwd=top_dir)
4980 if opts.dry_run and gn_ret == 2:
4981 return_value = 2 # Not formatted.
4982 elif opts.diff and gn_ret == 2:
4983 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004984 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07004985 elif gn_ret != 0:
4986 # For non-dry run cases (and non-2 return values for dry-run), a
4987 # nonzero error code indicates a failure, probably because the file
4988 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004989 DieWithError('gn format failed on ' + gn_diff_file +
4990 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004991
Ilya Shermane081cbe2017-08-15 17:51:04 -07004992 # Skip the metrics formatting from the global presubmit hook. These files have
4993 # a separate presubmit hook that issues an error if the files need formatting,
4994 # whereas the top-level presubmit script merely issues a warning. Formatting
4995 # these files is somewhat slow, so it's important not to duplicate the work.
4996 if not opts.presubmit:
4997 for xml_dir in GetDirtyMetricsDirs(diff_files):
4998 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00004999 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
5000 cmd = ['vpython', pretty_print_tool, '--non-interactive']
Ilya Shermane081cbe2017-08-15 17:51:04 -07005001 if opts.dry_run or opts.diff:
5002 cmd.append('--diff')
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005003 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
5004 # `shell` param and instead replace `'vpython'` with
5005 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
5006 stdout = RunCommand(cmd, cwd=top_dir,
5007 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07005008 if opts.diff:
5009 sys.stdout.write(stdout)
5010 if opts.dry_run and stdout:
5011 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005012
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005013 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005014
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005015
Steven Holte2e664bf2017-04-21 13:10:47 -07005016def GetDirtyMetricsDirs(diff_files):
5017 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5018 metrics_xml_dirs = [
5019 os.path.join('tools', 'metrics', 'actions'),
5020 os.path.join('tools', 'metrics', 'histograms'),
5021 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00005022 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005023 os.path.join('tools', 'metrics', 'ukm'),
5024 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005025 for xml_dir in metrics_xml_dirs:
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005026 if any(
5027 os.path.normpath(file).startswith(xml_dir) for file in xml_diff_files):
Steven Holte2e664bf2017-04-21 13:10:47 -07005028 yield xml_dir
5029
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005030
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005031@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005032@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005033def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005034 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005035 _, args = parser.parse_args(args)
5036
5037 if len(args) != 1:
5038 parser.print_help()
5039 return 1
5040
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005041 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005042 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005043 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005044
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005045 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005046
Edward Lemur52969c92020-02-06 18:15:28 +00005047 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00005048 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00005049 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005050
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005051 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005052 for key, issue in [x.split() for x in output.splitlines()]:
5053 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00005054 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00005055
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005056 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005057 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005058 return 1
5059 if len(branches) == 1:
5060 RunGit(['checkout', branches[0]])
5061 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005062 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005063 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005064 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00005065 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005066 try:
5067 RunGit(['checkout', branches[int(which)]])
5068 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005069 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005070 return 1
5071
5072 return 0
5073
5074
maruel@chromium.org29404b52014-09-08 22:58:00 +00005075def CMDlol(parser, args):
5076 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005077 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005078 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5079 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5080 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005081 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005082 return 0
5083
5084
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005085class OptionParser(optparse.OptionParser):
5086 """Creates the option parse and add --verbose support."""
5087 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005088 optparse.OptionParser.__init__(
5089 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005090 self.add_option(
5091 '-v', '--verbose', action='count', default=0,
5092 help='Use 2 times for more debugging info')
5093
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005094 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005095 try:
5096 return self._parse_args(args)
5097 finally:
5098 # Regardless of success or failure of args parsing, we want to report
5099 # metrics, but only after logging has been initialized (if parsing
5100 # succeeded).
5101 global settings
5102 settings = Settings()
5103
5104 if not metrics.DISABLE_METRICS_COLLECTION:
5105 # GetViewVCUrl ultimately calls logging method.
5106 project_url = settings.GetViewVCUrl().strip('/+')
5107 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5108 metrics.collector.add('project_urls', [project_url])
5109
5110 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005111 # Create an optparse.Values object that will store only the actual passed
5112 # options, without the defaults.
5113 actual_options = optparse.Values()
5114 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5115 # Create an optparse.Values object with the default options.
5116 options = optparse.Values(self.get_default_values().__dict__)
5117 # Update it with the options passed by the user.
5118 options._update_careful(actual_options.__dict__)
5119 # Store the options passed by the user in an _actual_options attribute.
5120 # We store only the keys, and not the values, since the values can contain
5121 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005122 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005123
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005124 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005125 logging.basicConfig(
5126 level=levels[min(options.verbose, len(levels) - 1)],
5127 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5128 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005129
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005130 return options, args
5131
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005133def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005134 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005135 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005136 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005137 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005138
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005139 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005140 dispatcher = subcommand.CommandDispatcher(__name__)
5141 try:
5142 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005143 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005144 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005145 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005146 if e.code != 500:
5147 raise
5148 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005149 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005150 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005151 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005152
5153
5154if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005155 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5156 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005157 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005158 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005159 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005160 sys.exit(main(sys.argv[1:]))