blob: 24e3b9a0f8c31dd3702c5988601a1e9d4842d6b1 [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
Edward Lesmeseeca9c62020-11-20 00:00:17 +000045import owners_client
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
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000111DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
112DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
113
thestig@chromium.org44202a22014-03-11 19:22:18 +0000114# Valid extensions for files we want to lint.
115DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
116DEFAULT_LINT_IGNORE_REGEX = r"$^"
117
Aiden Bennerc08566e2018-10-03 17:52:42 +0000118# File name for yapf style config files.
119YAPF_CONFIG_FILENAME = '.style.yapf'
120
Edward Lesmes50da7702020-03-30 19:23:43 +0000121# The issue, patchset and codereview server are stored on git config for each
122# branch under branch.<branch-name>.<config-key>.
123ISSUE_CONFIG_KEY = 'gerritissue'
124PATCHSET_CONFIG_KEY = 'gerritpatchset'
125CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
126
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000127# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000128Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000129
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000130# Initialized in main()
131settings = None
132
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100133# Used by tests/git_cl_test.py to add extra logging.
134# Inside the weirdly failing test, add this:
135# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700136# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100137_IS_BEING_TESTED = False
138
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000139
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000140_KNOWN_GERRIT_TO_SHORT_URLS = {
141 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
142 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
143}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000144assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
145 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000146
147
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000148class GitPushError(Exception):
149 pass
150
151
Christopher Lamf732cd52017-01-24 12:40:11 +1100152def DieWithError(message, change_desc=None):
153 if change_desc:
154 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000155 print('\n ** Content of CL description **\n' +
156 '='*72 + '\n' +
157 change_desc.description + '\n' +
158 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100159
vapiera7fbd5a2016-06-16 09:17:49 -0700160 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161 sys.exit(1)
162
163
Christopher Lamf732cd52017-01-24 12:40:11 +1100164def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000165 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000166 print('\nsaving CL description to %s\n' % backup_path)
Josip906bfde2020-01-31 22:38:49 +0000167 with open(backup_path, 'w') as backup_file:
168 backup_file.write(change_desc.description)
Christopher Lamf732cd52017-01-24 12:40:11 +1100169
170
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000171def GetNoGitPagerEnv():
172 env = os.environ.copy()
173 # 'cat' is a magical git string that disables pagers on all platforms.
174 env['GIT_PAGER'] = 'cat'
175 return env
176
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000177
bsep@chromium.org627d9002016-04-29 00:00:52 +0000178def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000179 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000180 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
181 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000182 except subprocess2.CalledProcessError as e:
183 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000184 if not error_ok:
Alan Cutter594fd332020-07-21 23:55:27 +0000185 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
186 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
Edward Lemur79d4f992019-11-11 23:49:02 +0000187 return e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000188
189
190def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000191 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000192 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000193
194
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000195def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000196 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700197 if suppress_stderr:
Edward Lesmescf06cad2020-12-14 22:03:23 +0000198 stderr = subprocess2.DEVNULL
tandrii5d48c322016-08-18 16:19:37 -0700199 else:
200 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000201 try:
tandrii5d48c322016-08-18 16:19:37 -0700202 (out, _), code = subprocess2.communicate(['git'] + args,
203 env=GetNoGitPagerEnv(),
204 stdout=subprocess2.PIPE,
205 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000206 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700207 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900208 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000209 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000210
211
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000212def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000213 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000214 return RunGitWithCode(args, suppress_stderr=True)[1]
215
216
tandrii2a16b952016-10-19 07:09:44 -0700217def time_sleep(seconds):
218 # Use this so that it can be mocked in tests without interfering with python
219 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700220 return time.sleep(seconds)
221
222
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000223def time_time():
224 # Use this so that it can be mocked in tests without interfering with python
225 # system machinery.
226 return time.time()
227
228
Edward Lemur1b52d872019-05-09 21:12:12 +0000229def datetime_now():
230 # Use this so that it can be mocked in tests without interfering with python
231 # system machinery.
232 return datetime.datetime.now()
233
234
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100235def confirm_or_exit(prefix='', action='confirm'):
236 """Asks user to press enter to continue or press Ctrl+C to abort."""
237 if not prefix or prefix.endswith('\n'):
238 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100239 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100240 mid = ' Press'
241 elif prefix.endswith(' '):
242 mid = 'press'
243 else:
244 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000245 gclient_utils.AskForData(
246 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100247
248
249def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000250 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000251 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100252 while True:
253 if 'yes'.startswith(result):
254 return True
255 if 'no'.startswith(result):
256 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000257 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000261 prop_list = getattr(options, 'properties', [])
262 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000263 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000264 try:
265 properties[key] = json.loads(val)
266 except ValueError:
267 pass # If a value couldn't be evaluated, treat it as a string.
268 return properties
269
270
Edward Lemur4c707a22019-09-24 21:13:43 +0000271def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000272 """Calls a buildbucket v2 method and returns the parsed json response."""
273 headers = {
274 'Accept': 'application/json',
275 'Content-Type': 'application/json',
276 }
277 request = json.dumps(request)
278 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
279
280 logging.info('POST %s with %s' % (url, request))
281
282 attempts = 1
283 time_to_sleep = 1
284 while True:
285 response, content = http.request(url, 'POST', body=request, headers=headers)
286 if response.status == 200:
287 return json.loads(content[4:])
288 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
289 msg = '%s error when calling POST %s with %s: %s' % (
290 response.status, url, request, content)
291 raise BuildbucketResponseException(msg)
292 logging.debug(
293 '%s error when calling POST %s with %s. '
294 'Sleeping for %d seconds and retrying...' % (
295 response.status, url, request, time_to_sleep))
296 time.sleep(time_to_sleep)
297 time_to_sleep *= 2
298 attempts += 1
299
300 assert False, 'unreachable'
301
302
Edward Lemur6215c792019-10-03 21:59:05 +0000303def _parse_bucket(raw_bucket):
304 legacy = True
305 project = bucket = None
306 if '/' in raw_bucket:
307 legacy = False
308 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000309 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000310 elif raw_bucket.startswith('luci.'):
311 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000312 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000313 elif '.' in raw_bucket:
314 project = raw_bucket.split('.')[0]
315 bucket = raw_bucket
316 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000317 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000318 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
319 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000320
321
Quinten Yearsley777660f2020-03-04 23:37:06 +0000322def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000323 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700324
325 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000326 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000327 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700328 options: Command-line options.
329 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000330 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000331 for project, bucket, builder in jobs:
332 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000333 print('To see results here, run: git cl try-results')
334 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700335
Quinten Yearsley777660f2020-03-04 23:37:06 +0000336 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000337 if not requests:
338 return
339
Edward Lemur5b929a42019-10-21 17:57:39 +0000340 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000341 http.force_exception_to_status_code = True
342
343 batch_request = {'requests': requests}
344 batch_response = _call_buildbucket(
345 http, options.buildbucket_host, 'Batch', batch_request)
346
347 errors = [
348 ' ' + response['error']['message']
349 for response in batch_response.get('responses', [])
350 if 'error' in response
351 ]
352 if errors:
353 raise BuildbucketResponseException(
354 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
355
356
Quinten Yearsley777660f2020-03-04 23:37:06 +0000357def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000358 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000359 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000360 shared_properties = {
361 'category': options.ensure_value('category', 'git_cl_try')
362 }
363 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000364 shared_properties['clobber'] = True
365 shared_properties.update(_get_properties_from_options(options) or {})
366
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000367 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000368 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000369 shared_tags.append({'key': 'retry_failed',
370 'value': '1'})
371
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000372 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000373 for (project, bucket, builder) in jobs:
374 properties = shared_properties.copy()
375 if 'presubmit' in builder.lower():
376 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000377
Edward Lemur45768512020-03-02 19:03:14 +0000378 requests.append({
379 'scheduleBuild': {
380 'requestId': str(uuid.uuid4()),
381 'builder': {
382 'project': getattr(options, 'project', None) or project,
383 'bucket': bucket,
384 'builder': builder,
385 },
386 'gerritChanges': gerrit_changes,
387 'properties': properties,
388 'tags': [
389 {'key': 'builder', 'value': builder},
390 ] + shared_tags,
391 }
392 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000393
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000394 if options.ensure_value('revision', None):
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000395 remote, remote_branch = changelist.GetRemoteBranch()
Edward Lemur45768512020-03-02 19:03:14 +0000396 requests[-1]['scheduleBuild']['gitilesCommit'] = {
397 'host': gerrit_changes[0]['host'],
398 'project': gerrit_changes[0]['project'],
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000399 'id': options.revision,
400 'ref': GetTargetRef(remote, remote_branch, None)
Edward Lemur45768512020-03-02 19:03:14 +0000401 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000402
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000403 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000404
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000405
Quinten Yearsley777660f2020-03-04 23:37:06 +0000406def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000407 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000408
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000409 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000410 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000411 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000412 request = {
413 'predicate': {
414 'gerritChanges': [changelist.GetGerritChange(patchset)],
415 },
416 'fields': ','.join('builds.*.' + field for field in fields),
417 }
tandrii221ab252016-10-06 08:12:04 -0700418
Edward Lemur5b929a42019-10-21 17:57:39 +0000419 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000420 if authenticator.has_cached_credentials():
421 http = authenticator.authorize(httplib2.Http())
422 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700423 print('Warning: Some results might be missing because %s' %
424 # Get the message on how to login.
Andrii Shyshkalov2517afd2021-01-19 17:07:43 +0000425 (str(auth.LoginRequiredError()),))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000426 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000427 http.force_exception_to_status_code = True
428
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000429 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
430 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000431
Edward Lemur45768512020-03-02 19:03:14 +0000432
Edward Lemur5b929a42019-10-21 17:57:39 +0000433def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000434 """Fetches builds from the latest patchset that has builds (within
435 the last few patchsets).
436
437 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000438 changelist (Changelist): The CL to fetch builds for
439 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000440 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
441 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000442 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000443 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
444 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000445 """
446 assert buildbucket_host
447 assert changelist.GetIssue(), 'CL must be uploaded first'
448 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000449 if latest_patchset is None:
450 assert changelist.GetMostRecentPatchset()
451 ps = changelist.GetMostRecentPatchset()
452 else:
453 assert latest_patchset > 0, latest_patchset
454 ps = latest_patchset
455
Quinten Yearsley983111f2019-09-26 17:18:48 +0000456 min_ps = max(1, ps - 5)
457 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000458 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000459 if len(builds):
460 return builds, ps
461 ps -= 1
462 return [], 0
463
464
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000465def _filter_failed_for_retry(all_builds):
466 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000467
468 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000469 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000470 i.e. a list of buildbucket.v2.Builds which includes status and builder
471 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000472
473 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000474 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000475 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000476 """
Edward Lemur45768512020-03-02 19:03:14 +0000477 grouped = {}
478 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000479 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000480 key = (builder['project'], builder['bucket'], builder['builder'])
481 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000482
Edward Lemur45768512020-03-02 19:03:14 +0000483 jobs = []
484 for (project, bucket, builder), builds in grouped.items():
485 if 'triggered' in builder:
486 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
487 'from a parent. Please schedule a manual job for the parent '
488 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000489 continue
490 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
491 # Don't retry if any are running.
492 continue
Edward Lemur45768512020-03-02 19:03:14 +0000493 # If builder had several builds, retry only if the last one failed.
494 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
495 # build, but in case of retrying failed jobs retrying a flaky one makes
496 # sense.
497 builds = sorted(builds, key=lambda b: b['createTime'])
498 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
499 continue
500 # Don't retry experimental build previously triggered by CQ.
501 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
502 for t in builds[-1]['tags']):
503 continue
504 jobs.append((project, bucket, builder))
505
506 # Sort the jobs to make testing easier.
507 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000508
509
Quinten Yearsley777660f2020-03-04 23:37:06 +0000510def _print_tryjobs(options, builds):
511 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000513 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 return
515
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000516 longest_builder = max(len(b['builder']['builder']) for b in builds)
517 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000519 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
520 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000522 builds_by_status = {}
523 for b in builds:
524 builds_by_status.setdefault(b['status'], []).append({
525 'id': b['id'],
526 'name': name_fmt.format(
527 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
528 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000530 sort_key = lambda b: (b['name'], b['id'])
531
532 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000534 if not builds:
535 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000537 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000538 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000539 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 else:
541 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
542
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000543 print(colorize(title))
544 for b in sorted(builds, key=sort_key):
545 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546
547 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000548 print_builds(
549 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
550 print_builds(
551 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
552 color=Fore.MAGENTA)
553 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
554 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
555 color=Fore.MAGENTA)
Andrii Shyshkalov792630c2020-10-19 16:47:44 +0000556 print_builds('Started:', builds_by_status.pop('STARTED', []),
557 color=Fore.YELLOW)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000558 print_builds(
559 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000561 print_builds(
562 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000563 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564
565
Aiden Bennerc08566e2018-10-03 17:52:42 +0000566def _ComputeDiffLineRanges(files, upstream_commit):
567 """Gets the changed line ranges for each file since upstream_commit.
568
569 Parses a git diff on provided files and returns a dict that maps a file name
570 to an ordered list of range tuples in the form (start_line, count).
571 Ranges are in the same format as a git diff.
572 """
573 # If files is empty then diff_output will be a full diff.
574 if len(files) == 0:
575 return {}
576
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000577 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000578 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000579 diff_output = RunGit(diff_cmd)
580
581 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
582 # 2 capture groups
583 # 0 == fname of diff file
584 # 1 == 'diff_start,diff_count' or 'diff_start'
585 # will match each of
586 # diff --git a/foo.foo b/foo.py
587 # @@ -12,2 +14,3 @@
588 # @@ -12,2 +17 @@
589 # running re.findall on the above string with pattern will give
590 # [('foo.py', ''), ('', '14,3'), ('', '17')]
591
592 curr_file = None
593 line_diffs = {}
594 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
595 if match[0] != '':
596 # Will match the second filename in diff --git a/a.py b/b.py.
597 curr_file = match[0]
598 line_diffs[curr_file] = []
599 else:
600 # Matches +14,3
601 if ',' in match[1]:
602 diff_start, diff_count = match[1].split(',')
603 else:
604 # Single line changes are of the form +12 instead of +12,1.
605 diff_start = match[1]
606 diff_count = 1
607
608 diff_start = int(diff_start)
609 diff_count = int(diff_count)
610
611 # If diff_count == 0 this is a removal we can ignore.
612 line_diffs[curr_file].append((diff_start, diff_count))
613
614 return line_diffs
615
616
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000617def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000618 """Checks if a yapf file is in any parent directory of fpath until top_dir.
619
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000620 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000621 is found returns None. Uses yapf_config_cache as a cache for previously found
622 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000623 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000624 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000625 # Return result if we've already computed it.
626 if fpath in yapf_config_cache:
627 return yapf_config_cache[fpath]
628
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000629 parent_dir = os.path.dirname(fpath)
630 if os.path.isfile(fpath):
631 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000632 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000633 # Otherwise fpath is a directory
634 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
635 if os.path.isfile(yapf_file):
636 ret = yapf_file
637 elif fpath == top_dir or parent_dir == fpath:
638 # If we're at the top level directory, or if we're at root
639 # there is no provided style.
640 ret = None
641 else:
642 # Otherwise recurse on the current directory.
643 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000644 yapf_config_cache[fpath] = ret
645 return ret
646
647
Brian Sheedyb4307d52019-12-02 19:18:17 +0000648def _GetYapfIgnorePatterns(top_dir):
649 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000650
651 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
652 but this functionality appears to break when explicitly passing files to
653 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000654 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000655 the .yapfignore file should be in the directory that yapf is invoked from,
656 which we assume to be the top level directory in this case.
657
658 Args:
659 top_dir: The top level directory for the repository being formatted.
660
661 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000662 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000663 """
664 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000665 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000666 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000667 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000668
Anthony Politoc64e3902021-04-30 21:55:25 +0000669 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
670 stripped_line = line.strip()
671 # Comments and blank lines should be ignored.
672 if stripped_line.startswith('#') or stripped_line == '':
673 continue
674 ignore_patterns.add(stripped_line)
Brian Sheedyb4307d52019-12-02 19:18:17 +0000675 return ignore_patterns
676
677
678def _FilterYapfIgnoredFiles(filepaths, patterns):
679 """Filters out any filepaths that match any of the given patterns.
680
681 Args:
682 filepaths: An iterable of strings containing filepaths to filter.
683 patterns: An iterable of strings containing fnmatch patterns to filter on.
684
685 Returns:
686 A list of strings containing all the elements of |filepaths| that did not
687 match any of the patterns in |patterns|.
688 """
689 # Not inlined so that tests can use the same implementation.
690 return [f for f in filepaths
691 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000692
693
Aaron Gable13101a62018-02-09 13:20:41 -0800694def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000695 """Prints statistics about the change to the user."""
696 # --no-ext-diff is broken in some versions of Git, so try to work around
697 # this by overriding the environment (but there is still a problem if the
698 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000699 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000700 if 'GIT_EXTERNAL_DIFF' in env:
701 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000702
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000703 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800704 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000705 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000706
707
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000708class BuildbucketResponseException(Exception):
709 pass
710
711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712class Settings(object):
713 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000714 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000715 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716 self.tree_status_url = None
717 self.viewvc_url = None
718 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000719 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000720 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000721 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000722 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000723 self.format_full_by_default = None
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000724 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
Edward Lemur26964072020-02-19 19:18:51 +0000726 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000728 if self.updated:
729 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000730
Edward Lemur26964072020-02-19 19:18:51 +0000731 # The only value that actually changes the behavior is
732 # autoupdate = "false". Everything else means "true".
733 autoupdate = (
734 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
735
736 cr_settings_file = FindCodereviewSettingsFile()
737 if autoupdate != 'false' and cr_settings_file:
738 LoadCodereviewSettingsFromFile(cr_settings_file)
739 cr_settings_file.close()
740
741 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000743 @staticmethod
744 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000745 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000748 if self.root is None:
749 self.root = os.path.abspath(self.GetRelativeRoot())
750 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 def GetTreeStatusUrl(self, error_ok=False):
753 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000754 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
755 if self.tree_status_url is None and not error_ok:
756 DieWithError(
757 'You must configure your tree status URL by running '
758 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000759 return self.tree_status_url
760
761 def GetViewVCUrl(self):
762 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000763 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 return self.viewvc_url
765
rmistry@google.com90752582014-01-14 21:04:50 +0000766 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000767 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000768
rmistry@google.com5626a922015-02-26 14:03:30 +0000769 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000770 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000771 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000772 return run_post_upload_hook == "True"
773
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000774 def GetDefaultCCList(self):
Edward Lemur26964072020-02-19 19:18:51 +0000775 return self._GetConfig('rietveld.cc')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000776
Dirk Pranke6f0df682021-06-25 00:42:33 +0000777 def GetUsePython3(self):
778 return self._GetConfig('rietveld.use-python3')
779
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000780 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000781 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000782 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000783 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
784 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000785 # Default is squash now (http://crbug.com/611892#c23).
786 self.squash_gerrit_uploads = self._GetConfig(
787 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000788 return self.squash_gerrit_uploads
789
Edward Lesmes4de54132020-05-05 19:41:33 +0000790 def GetSquashGerritUploadsOverride(self):
791 """Return True or False if codereview.settings should be overridden.
792
793 Returns None if no override has been defined.
794 """
795 # See also http://crbug.com/611892#c23
796 result = self._GetConfig('gerrit.override-squash-uploads').lower()
797 if result == 'true':
798 return True
799 if result == 'false':
800 return False
801 return None
802
tandrii@chromium.org28253532016-04-14 13:46:56 +0000803 def GetGerritSkipEnsureAuthenticated(self):
804 """Return True if EnsureAuthenticated should not be done for Gerrit
805 uploads."""
806 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000807 self.gerrit_skip_ensure_authenticated = self._GetConfig(
808 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000809 return self.gerrit_skip_ensure_authenticated
810
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000811 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000812 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000813 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000814 # Git requires single quotes for paths with spaces. We need to replace
815 # them with double quotes for Windows to treat such paths as a single
816 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000817 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000818 return self.git_editor or None
819
thestig@chromium.org44202a22014-03-11 19:22:18 +0000820 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000821 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000822
823 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000824 return self._GetConfig(
825 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000826
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000827 def GetFormatFullByDefault(self):
828 if self.format_full_by_default is None:
829 result = (
830 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
831 error_ok=True).strip())
832 self.format_full_by_default = (result == 'true')
833 return self.format_full_by_default
834
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000835 def IsStatusCommitOrderByDate(self):
836 if self.is_status_commit_order_by_date is None:
837 result = (RunGit(['config', '--bool', 'cl.date-order'],
838 error_ok=True).strip())
839 self.is_status_commit_order_by_date = (result == 'true')
840 return self.is_status_commit_order_by_date
841
Edward Lemur26964072020-02-19 19:18:51 +0000842 def _GetConfig(self, key, default=''):
843 self._LazyUpdateIfNeeded()
844 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845
846
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000847class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000848 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000849 NONE = 'none'
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000850 QUICK_RUN = 'quick_run'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000851 DRY_RUN = 'dry_run'
852 COMMIT = 'commit'
853
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000854 ALL_STATES = [NONE, QUICK_RUN, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000855
856
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000857class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000858 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000859 self.issue = issue
860 self.patchset = patchset
861 self.hostname = hostname
862
863 @property
864 def valid(self):
865 return self.issue is not None
866
867
Edward Lemurf38bc172019-09-03 21:02:13 +0000868def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000869 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
870 fail_result = _ParsedIssueNumberArgument()
871
Edward Lemur678a6842019-10-03 22:25:05 +0000872 if isinstance(arg, int):
873 return _ParsedIssueNumberArgument(issue=arg)
874 if not isinstance(arg, basestring):
875 return fail_result
876
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000877 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000878 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000879 if not arg.startswith('http'):
880 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700881
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000882 url = gclient_utils.UpgradeToHttps(arg)
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000883 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
884 if url.startswith(short_url):
885 url = gerrit_url + url[len(short_url):]
886 break
887
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000888 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000889 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000890 except ValueError:
891 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200892
Edward Lemur678a6842019-10-03 22:25:05 +0000893 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
894 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
895 # Short urls like https://domain/<issue_number> can be used, but don't allow
896 # specifying the patchset (you'd 404), but we allow that here.
897 if parsed_url.path == '/':
898 part = parsed_url.fragment
899 else:
900 part = parsed_url.path
901
902 match = re.match(
903 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
904 if not match:
905 return fail_result
906
907 issue = int(match.group('issue'))
908 patchset = match.group('patchset')
909 return _ParsedIssueNumberArgument(
910 issue=issue,
911 patchset=int(patchset) if patchset else None,
912 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000913
914
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000915def _create_description_from_log(args):
916 """Pulls out the commit log to use as a base for the CL description."""
917 log_args = []
918 if len(args) == 1 and not args[0].endswith('.'):
919 log_args = [args[0] + '..']
920 elif len(args) == 1 and args[0].endswith('...'):
921 log_args = [args[0][:-1]]
922 elif len(args) == 2:
923 log_args = [args[0] + '..' + args[1]]
924 else:
925 log_args = args[:] # Hope for the best!
Manh Nguyene3644862020-08-05 18:25:46 +0000926 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000927
928
Aaron Gablea45ee112016-11-22 15:14:38 -0800929class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700930 def __init__(self, issue, url):
931 self.issue = issue
932 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800933 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700934
935 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800936 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700937 self.issue, self.url)
938
939
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100940_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +0000941 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100942 # TODO(tandrii): these two aren't known in Gerrit.
943 'approval', 'disapproval'])
944
945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000947 """Changelist works with one changelist in local branch.
948
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000949 Notes:
950 * Not safe for concurrent multi-{thread,process} use.
951 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700952 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000953 """
954
Sigurd Schneider9abde8c2020-11-17 08:44:52 +0000955 def __init__(self,
956 branchref=None,
957 issue=None,
958 codereview_host=None,
959 commit_date=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000960 """Create a new ChangeList instance.
961
Edward Lemurf38bc172019-09-03 21:02:13 +0000962 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000963 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000965 global settings
966 if not settings:
967 # Happens when git_cl.py is used as a utility library.
968 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 self.branchref = branchref
971 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000972 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +0000973 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 else:
975 self.branch = None
Sigurd Schneider9abde8c2020-11-17 08:44:52 +0000976 self.commit_date = commit_date
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000978 self.lookedup_issue = False
979 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000981 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000983 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -0800984 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000985 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +0000986 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000987
Edward Lemur125d60a2019-09-13 18:25:41 +0000988 # Lazily cached values.
989 self._gerrit_host = None # e.g. chromium-review.googlesource.com
990 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Edward Lesmese1576912021-02-16 21:53:34 +0000991 self._owners_client = None
Edward Lemur125d60a2019-09-13 18:25:41 +0000992 # Map from change number (issue) to its detail cache.
993 self._detail_cache = {}
994
995 if codereview_host is not None:
996 assert not codereview_host.startswith('https://'), codereview_host
997 self._gerrit_host = codereview_host
998 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000999
Edward Lesmese1576912021-02-16 21:53:34 +00001000 @property
1001 def owners_client(self):
1002 if self._owners_client is None:
1003 remote, remote_branch = self.GetRemoteBranch()
1004 branch = GetTargetRef(remote, remote_branch, None)
1005 self._owners_client = owners_client.GetCodeOwnersClient(
1006 root=settings.GetRoot(),
Edward Lesmes1eaaab52021-03-02 23:52:54 +00001007 upstream=self.GetCommonAncestorWithUpstream(),
Edward Lesmese1576912021-02-16 21:53:34 +00001008 host=self.GetGerritHost(),
1009 project=self.GetGerritProject(),
1010 branch=branch)
1011 return self._owners_client
1012
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001013 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001014 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001015
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001016 The return value is a string suitable for passing to git cl with the --cc
1017 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001018 """
1019 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001020 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001021 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001022 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1023 return self.cc
1024
Daniel Cheng7227d212017-11-17 08:12:37 -08001025 def ExtendCC(self, more_cc):
1026 """Extends the list of users to cc on this CL based on the changed files."""
1027 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001029 def GetCommitDate(self):
1030 """Returns the commit date as provided in the constructor"""
1031 return self.commit_date
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 def GetBranch(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001034 """Returns the short branch name, e.g. 'main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +00001036 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001037 if not branchref:
1038 return None
1039 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +00001040 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 return self.branch
1042
1043 def GetBranchRef(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001044 """Returns the full branch name, e.g. 'refs/heads/main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 self.GetBranch() # Poke the lazy loader.
1046 return self.branchref
1047
Edward Lemur85153282020-02-14 22:06:29 +00001048 def _GitGetBranchConfigValue(self, key, default=None):
1049 return scm.GIT.GetBranchConfig(
1050 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001051
Edward Lemur85153282020-02-14 22:06:29 +00001052 def _GitSetBranchConfigValue(self, key, value):
1053 action = 'set %s to %r' % (key, value)
1054 if not value:
1055 action = 'unset %s' % key
1056 assert self.GetBranch(), 'a branch is needed to ' + action
1057 return scm.GIT.SetBranchConfig(
1058 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001059
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001060 @staticmethod
1061 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001062 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001063 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001065 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1066 settings.GetRoot(), branch)
1067 if not remote or not upstream_branch:
1068 DieWithError(
1069 'Unable to determine default branch to diff against.\n'
Josip Sokcevicb038f722021-01-06 18:28:11 +00001070 'Verify this branch is set up to track another \n'
1071 '(via the --track argument to "git checkout -b ..."). \n'
1072 'or pass complete "git diff"-style arguments if supported, like\n'
1073 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074
1075 return remote, upstream_branch
1076
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001077 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001078 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001079 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001080 DieWithError('The upstream for the current branch (%s) does not exist '
1081 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001082 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001083 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001084
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 def GetUpstreamBranch(self):
1086 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001087 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001088 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 upstream_branch = upstream_branch.replace('refs/heads/',
1090 'refs/remotes/%s/' % remote)
1091 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1092 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 self.upstream_branch = upstream_branch
1094 return self.upstream_branch
1095
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001096 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001097 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 remote, branch = None, self.GetBranch()
1099 seen_branches = set()
1100 while branch not in seen_branches:
1101 seen_branches.add(branch)
1102 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001103 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001104 if remote != '.' or branch.startswith('refs/remotes'):
1105 break
1106 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001107 remotes = RunGit(['remote'], error_ok=True).split()
1108 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001109 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001110 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001111 remote = 'origin'
Gavin Make6a62332020-12-04 21:57:10 +00001112 logging.warning('Could not determine which remote this change is '
1113 'associated with, so defaulting to "%s".' %
1114 self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001115 else:
Gavin Make6a62332020-12-04 21:57:10 +00001116 logging.warning('Could not determine which remote this change is '
1117 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001118 branch = 'HEAD'
1119 if branch.startswith('refs/remotes'):
1120 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001121 elif branch.startswith('refs/branch-heads/'):
1122 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001123 else:
1124 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001125 return self._remote
1126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 def GetRemoteUrl(self):
1128 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1129
1130 Returns None if there is no remote.
1131 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001132 is_cached, value = self._cached_remote_url
1133 if is_cached:
1134 return value
1135
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001136 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001137 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001138
Edward Lemur298f2cf2019-02-22 21:40:39 +00001139 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001140 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001141 if host:
1142 self._cached_remote_url = (True, url)
1143 return url
1144
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001145 # If it cannot be parsed as an url, assume it is a local directory,
1146 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001147 logging.warning('"%s" doesn\'t appear to point to a git host. '
1148 'Interpreting it as a local directory.', url)
1149 if not os.path.isdir(url):
1150 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001151 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1152 'but it doesn\'t exist.',
1153 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001154 return None
1155
1156 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001157 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001158
Edward Lemur79d4f992019-11-11 23:49:02 +00001159 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001160 if not host:
1161 logging.error(
1162 'Remote "%(remote)s" for branch "%(branch)s" points to '
1163 '"%(cache_path)s", but it is misconfigured.\n'
1164 '"%(cache_path)s" must be a git repo and must have a remote named '
1165 '"%(remote)s" pointing to the git host.', {
1166 'remote': remote,
1167 'cache_path': cache_path,
1168 'branch': self.GetBranch()})
1169 return None
1170
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001171 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001172 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001174 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001175 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001176 if self.issue is None and not self.lookedup_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00001177 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001178 if self.issue is not None:
1179 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001180 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 return self.issue
1182
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001183 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001185 issue = self.GetIssue()
1186 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001187 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001188 server = self.GetCodereviewServer()
1189 if short:
1190 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1191 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192
Dirk Pranke6f0df682021-06-25 00:42:33 +00001193 def GetUsePython3(self):
1194 return settings.GetUsePython3()
1195
Edward Lemur6c6827c2020-02-06 21:15:18 +00001196 def FetchDescription(self, pretty=False):
1197 assert self.GetIssue(), 'issue is required to query Gerrit'
1198
Edward Lemur9aa1a962020-02-25 00:58:38 +00001199 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001200 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1201 current_rev = data['current_revision']
1202 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001203
1204 if not pretty:
1205 return self.description
1206
1207 # Set width to 72 columns + 2 space indent.
1208 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1209 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1210 lines = self.description.splitlines()
1211 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212
1213 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001214 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001215 if self.patchset is None and not self.lookedup_patchset:
Edward Lesmes50da7702020-03-30 19:23:43 +00001216 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001217 if self.patchset is not None:
1218 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001219 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 return self.patchset
1221
Edward Lemur9aa1a962020-02-25 00:58:38 +00001222 def GetAuthor(self):
1223 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001226 """Set this branch's patchset. If patchset=0, clears the patchset."""
1227 assert self.GetBranch()
1228 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001230 else:
1231 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001232 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001234 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001235 """Set this branch's issue. If issue isn't given, clears the issue."""
1236 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001238 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001239 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001240 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001241 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001242 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001243 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001244 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
tandrii5d48c322016-08-18 16:19:37 -07001246 # Reset all of these just to be clean.
1247 reset_suffixes = [
1248 'last-upload-hash',
Edward Lesmes50da7702020-03-30 19:23:43 +00001249 ISSUE_CONFIG_KEY,
1250 PATCHSET_CONFIG_KEY,
1251 CODEREVIEW_SERVER_CONFIG_KEY,
1252 'gerritsquashhash',
1253 ]
tandrii5d48c322016-08-18 16:19:37 -07001254 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001255 try:
1256 self._GitSetBranchConfigValue(prop, None)
1257 except subprocess2.CalledProcessError:
1258 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001259 msg = RunGit(['log', '-1', '--format=%B']).strip()
1260 if msg and git_footers.get_footer_change_id(msg):
1261 print('WARNING: The change patched into this branch has a Change-Id. '
1262 'Removing it.')
1263 RunGit(['commit', '--amend', '-m',
1264 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001265 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001266 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001267 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268
Edward Lemur2c62b332020-03-12 22:12:33 +00001269 def GetAffectedFiles(self, upstream):
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001270 try:
Edward Lemur2c62b332020-03-12 22:12:33 +00001271 return [f for _, f in scm.GIT.CaptureStatus(settings.GetRoot(), upstream)]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001272 except subprocess2.CalledProcessError:
1273 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001274 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001275 'This branch probably doesn\'t exist anymore. To reset the\n'
1276 'tracking branch, please run\n'
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001277 ' git branch --set-upstream-to origin/main %s\n'
1278 'or replace origin/main with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001279 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001280
dsansomee2d6fd92016-09-08 00:10:47 -07001281 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001282 assert self.GetIssue(), 'issue is required to update description'
1283
1284 if gerrit_util.HasPendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001285 self.GetGerritHost(), self._GerritChangeIdentifier()):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001286 if not force:
1287 confirm_or_exit(
1288 'The description cannot be modified while the issue has a pending '
1289 'unpublished edit. Either publish the edit in the Gerrit web UI '
1290 'or delete it.\n\n', action='delete the unpublished edit')
1291
1292 gerrit_util.DeletePendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001293 self.GetGerritHost(), self._GerritChangeIdentifier())
Edward Lemur6c6827c2020-02-06 21:15:18 +00001294 gerrit_util.SetCommitMessage(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001295 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur6c6827c2020-02-06 21:15:18 +00001296 description, notify='NONE')
1297
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001299
Edward Lemur75526302020-02-27 22:31:05 +00001300 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001301 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001302 '--root', settings.GetRoot(),
1303 '--upstream', upstream,
1304 ]
1305
1306 args.extend(['--verbose'] * verbose)
1307
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001308 remote, remote_branch = self.GetRemoteBranch()
1309 target_ref = GetTargetRef(remote, remote_branch, None)
1310 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1311 args.extend(['--gerrit_project', self.GetGerritProject()])
1312 args.extend(['--gerrit_branch', target_ref])
1313
Edward Lemur99df04e2020-03-05 19:39:43 +00001314 author = self.GetAuthor()
Edward Lemur227d5102020-02-25 23:45:35 +00001315 issue = self.GetIssue()
1316 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001317 if author:
1318 args.extend(['--author', author])
Edward Lemur227d5102020-02-25 23:45:35 +00001319 if issue:
1320 args.extend(['--issue', str(issue)])
1321 if patchset:
1322 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001323
Edward Lemur75526302020-02-27 22:31:05 +00001324 return args
1325
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001326 def RunHook(self, committing, may_prompt, verbose, parallel, upstream,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001327 description, all_files, resultdb=False, realm=None):
Edward Lemur75526302020-02-27 22:31:05 +00001328 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1329 args = self._GetCommonPresubmitArgs(verbose, upstream)
1330 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001331 if may_prompt:
1332 args.append('--may_prompt')
1333 if parallel:
1334 args.append('--parallel')
1335 if all_files:
1336 args.append('--all_files')
1337
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001338 if resultdb and not realm:
1339 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1340 # it is not required to pass the realm flag
1341 print('Note: ResultDB reporting will NOT be performed because --realm'
1342 ' was not specified. To enable ResultDB, please run the command'
1343 ' again with the --realm argument to specify the LUCI realm.')
1344
1345 py2_results = self._RunPresubmit(args, resultdb, realm, description,
1346 use_python3=False)
1347 py3_results = self._RunPresubmit(args, resultdb, realm, description,
1348 use_python3=True)
1349 return self._MergePresubmitResults(py2_results, py3_results)
1350
1351 def _RunPresubmit(self, args, resultdb, realm, description, use_python3):
1352 args = args[:]
1353 vpython = 'vpython3' if use_python3 else 'vpython'
1354
Edward Lemur227d5102020-02-25 23:45:35 +00001355 with gclient_utils.temporary_file() as description_file:
1356 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001357 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001358 args.extend(['--json_output', json_output])
1359 args.extend(['--description_file', description_file])
Dirk Pranke6f0df682021-06-25 00:42:33 +00001360 if self.GetUsePython3():
1361 args.append('--use-python3')
Edward Lemur227d5102020-02-25 23:45:35 +00001362 start = time_time()
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001363 cmd = [vpython, PRESUBMIT_SUPPORT] + args
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001364 if resultdb and realm:
1365 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001366
1367 p = subprocess2.Popen(cmd)
Edward Lemur227d5102020-02-25 23:45:35 +00001368 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001369
Edward Lemur227d5102020-02-25 23:45:35 +00001370 metrics.collector.add_repeated('sub_commands', {
1371 'command': 'presubmit',
1372 'execution_time': time_time() - start,
1373 'exit_code': exit_code,
1374 })
1375
1376 if exit_code:
1377 sys.exit(exit_code)
1378
1379 json_results = gclient_utils.FileRead(json_output)
1380 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001381
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001382 def _MergePresubmitResults(self, py2_results, py3_results):
1383 return {
1384 'more_cc': sorted(set(py2_results.get('more_cc', []) +
1385 py3_results.get('more_cc', []))),
1386 'errors': (
1387 py2_results.get('errors', []) + py3_results.get('errors', [])),
1388 'notifications': (
1389 py2_results.get('notifications', []) +
1390 py3_results.get('notifications', [])),
1391 'warnings': (
1392 py2_results.get('warnings', []) + py3_results.get('warnings', []))
1393 }
1394
Edward Lemur75526302020-02-27 22:31:05 +00001395 def RunPostUploadHook(self, verbose, upstream, description):
1396 args = self._GetCommonPresubmitArgs(verbose, upstream)
1397 args.append('--post_upload')
1398
1399 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001400 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001401 args.extend(['--description_file', description_file])
1402 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1403 p.wait()
1404
Edward Lemur5a644f82020-03-18 16:44:57 +00001405 def _GetDescriptionForUpload(self, options, git_diff_args, files):
1406 # Get description message for upload.
1407 if self.GetIssue():
1408 description = self.FetchDescription()
1409 elif options.message:
1410 description = options.message
1411 else:
1412 description = _create_description_from_log(git_diff_args)
1413 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001414 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001415
1416 # Extract bug number from branch name.
1417 bug = options.bug
1418 fixed = options.fixed
1419 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
1420 self.GetBranch())
1421 if not bug and not fixed and match:
1422 if match.group('type') == 'bug':
1423 bug = match.group('bugnum')
1424 else:
1425 fixed = match.group('bugnum')
1426
1427 change_description = ChangeDescription(description, bug, fixed)
1428
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001429 # Fill gaps in OWNERS coverage to tbrs/reviewers if requested.
1430 if options.add_owners_to:
1431 assert options.add_owners_to in ('TBR', 'R'), options.add_owners_to
Edward Lesmese1576912021-02-16 21:53:34 +00001432 status = self.owners_client.GetFilesApprovalStatus(
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001433 files, [], options.tbrs + options.reviewers)
1434 missing_files = [
1435 f for f in files
Edward Lesmese1576912021-02-16 21:53:34 +00001436 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001437 ]
Edward Lesmese1576912021-02-16 21:53:34 +00001438 owners = self.owners_client.SuggestOwners(
1439 missing_files, exclude=[self.GetAuthor()])
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001440 if options.add_owners_to == 'TBR':
1441 assert isinstance(options.tbrs, list), options.tbrs
1442 options.tbrs.extend(owners)
1443 else:
1444 assert isinstance(options.reviewers, list), options.reviewers
1445 options.reviewers.extend(owners)
1446
Edward Lemur5a644f82020-03-18 16:44:57 +00001447 # Set the reviewer list now so that presubmit checks can access it.
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001448 if options.reviewers or options.tbrs:
1449 change_description.update_reviewers(options.reviewers, options.tbrs)
Edward Lemur5a644f82020-03-18 16:44:57 +00001450
1451 return change_description
1452
1453 def _GetTitleForUpload(self, options):
1454 # When not squashing, just return options.title.
1455 if not options.squash:
1456 return options.title
1457
1458 # On first upload, patchset title is always this string, while options.title
1459 # gets converted to first line of message.
1460 if not self.GetIssue():
1461 return 'Initial upload'
1462
1463 # When uploading subsequent patchsets, options.message is taken as the title
1464 # if options.title is not provided.
1465 if options.title:
1466 return options.title
1467 if options.message:
1468 return options.message.strip()
1469
1470 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001471 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00001472 if options.force or options.skip_title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001473 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001474 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
mlcui3da91712021-05-05 10:00:30 +00001475
1476 # Use the default title if the user confirms the default with a 'y'.
1477 if user_title.lower() == 'y':
1478 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001479 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001480
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001481 def CMDUpload(self, options, git_diff_args, orig_args):
1482 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001483 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001484 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001485 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001486 else:
1487 if self.GetBranch() is None:
1488 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1489
1490 # Default to diffing against common ancestor of upstream branch
1491 base_branch = self.GetCommonAncestorWithUpstream()
1492 git_diff_args = [base_branch, 'HEAD']
1493
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001494 # Fast best-effort checks to abort before running potentially expensive
1495 # hooks if uploading is likely to fail anyway. Passing these checks does
1496 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001497 self.EnsureAuthenticated(force=options.force)
1498 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001499
1500 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001501 watchlist = watchlists.Watchlists(settings.GetRoot())
1502 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001503 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001504 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001505
Edward Lemur5a644f82020-03-18 16:44:57 +00001506 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001507 if not options.bypass_hooks:
Edward Lemur2c62b332020-03-12 22:12:33 +00001508 hook_results = self.RunHook(
1509 committing=False,
1510 may_prompt=not options.force,
1511 verbose=options.verbose,
1512 parallel=options.parallel,
1513 upstream=base_branch,
Edward Lemur5a644f82020-03-18 16:44:57 +00001514 description=change_desc.description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001515 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001516 resultdb=options.resultdb,
1517 realm=options.realm)
Edward Lemur227d5102020-02-25 23:45:35 +00001518 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001519
Aaron Gable13101a62018-02-09 13:20:41 -08001520 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001521 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001522 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001523 if not ret:
Edward Lemur85153282020-02-14 22:06:29 +00001524 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001525 'last-upload-hash', scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001526 # Run post upload hooks, if specified.
1527 if settings.GetRunPostUploadHook():
Edward Lemur5a644f82020-03-18 16:44:57 +00001528 self.RunPostUploadHook(
1529 options.verbose, base_branch, change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001530
1531 # Upload all dependencies if specified.
1532 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001533 print()
1534 print('--dependencies has been specified.')
1535 print('All dependent local branches will be re-uploaded.')
1536 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001537 # Remove the dependencies flag from args so that we do not end up in a
1538 # loop.
1539 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001540 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001541 return ret
1542
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001543 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001544 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001545
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001546 Issue must have been already uploaded and known. Optionally allows for
1547 updating Quick-Run (QR) state.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001548 """
1549 assert new_state in _CQState.ALL_STATES
1550 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001551 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001552 vote_map = {
1553 _CQState.NONE: 0,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001554 _CQState.QUICK_RUN: 1,
Edward Lemur125d60a2019-09-13 18:25:41 +00001555 _CQState.DRY_RUN: 1,
1556 _CQState.COMMIT: 2,
1557 }
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001558 if new_state == _CQState.QUICK_RUN:
1559 labels = {
1560 'Commit-Queue': vote_map[_CQState.DRY_RUN],
1561 'Quick-Run': vote_map[_CQState.QUICK_RUN],
1562 }
1563 else:
1564 labels = {'Commit-Queue': vote_map[new_state]}
Edward Lemur125d60a2019-09-13 18:25:41 +00001565 notify = False if new_state == _CQState.DRY_RUN else None
1566 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001567 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur125d60a2019-09-13 18:25:41 +00001568 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001569 return 0
1570 except KeyboardInterrupt:
1571 raise
1572 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001573 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001574 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001575 ' * Your project has no CQ,\n'
1576 ' * You don\'t have permission to change the CQ state,\n'
1577 ' * There\'s a bug in this code (see stack trace below).\n'
1578 'Consider specifying which bots to trigger manually or asking your '
1579 'project owners for permissions or contacting Chrome Infra at:\n'
1580 'https://www.chromium.org/infra\n\n' %
1581 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001582 # Still raise exception so that stack trace is printed.
1583 raise
1584
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001585 def GetGerritHost(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001586 # Lazy load of configs.
1587 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001588 if self._gerrit_host and '.' not in self._gerrit_host:
1589 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1590 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001591 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001592 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001593 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001594 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001595 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1596 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001597 return self._gerrit_host
1598
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001599 def _GetGitHost(self):
1600 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001601 remote_url = self.GetRemoteUrl()
1602 if not remote_url:
1603 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001604 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001605
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001606 def GetCodereviewServer(self):
1607 if not self._gerrit_server:
1608 # If we're on a branch then get the server potentially associated
1609 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001610 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001611 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001612 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001613 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001614 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 if not self._gerrit_server:
1616 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1617 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001618 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001619 parts[0] = parts[0] + '-review'
1620 self._gerrit_host = '.'.join(parts)
1621 self._gerrit_server = 'https://%s' % self._gerrit_host
1622 return self._gerrit_server
1623
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001624 def GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001625 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001626 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001627 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001628 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001629 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001630 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001631 if project.endswith('.git'):
1632 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001633 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1634 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1635 # gitiles/git-over-https protocol. E.g.,
1636 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1637 # as
1638 # https://chromium.googlesource.com/v8/v8
1639 if project.startswith('a/'):
1640 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001641 return project
1642
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001643 def _GerritChangeIdentifier(self):
1644 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1645
1646 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001647 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001648 """
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001649 project = self.GetGerritProject()
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001650 if project:
1651 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1652 # Fall back on still unique, but less efficient change number.
1653 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001654
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001655 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001656 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001657 if settings.GetGerritSkipEnsureAuthenticated():
1658 # For projects with unusual authentication schemes.
1659 # See http://crbug.com/603378.
1660 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001661
1662 # Check presence of cookies only if using cookies-based auth method.
1663 cookie_auth = gerrit_util.Authenticator.get()
1664 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001665 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001666
Florian Mayerae510e82020-01-30 21:04:48 +00001667 remote_url = self.GetRemoteUrl()
1668 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001669 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001670 return
1671 if urllib.parse.urlparse(remote_url).scheme != 'https':
Josip906bfde2020-01-31 22:38:49 +00001672 logging.warning('Ignoring branch %(branch)s with non-https remote '
1673 '%(remote)s', {
1674 'branch': self.branch,
1675 'remote': self.GetRemoteUrl()
1676 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001677 return
1678
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001679 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001680 self.GetCodereviewServer()
1681 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001682 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001683
1684 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1685 git_auth = cookie_auth.get_auth_header(git_host)
1686 if gerrit_auth and git_auth:
1687 if gerrit_auth == git_auth:
1688 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001689 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001690 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001691 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001692 ' %s\n'
1693 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001694 ' Consider running the following command:\n'
1695 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001696 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001697 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001698 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001699 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001700 cookie_auth.get_new_password_message(git_host)))
1701 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001702 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001703 return
1704 else:
1705 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001706 ([] if gerrit_auth else [self._gerrit_host]) +
1707 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001708 DieWithError('Credentials for the following hosts are required:\n'
1709 ' %s\n'
1710 'These are read from %s (or legacy %s)\n'
1711 '%s' % (
1712 '\n '.join(missing),
1713 cookie_auth.get_gitcookies_path(),
1714 cookie_auth.get_netrc_path(),
1715 cookie_auth.get_new_password_message(git_host)))
1716
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001717 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001718 if not self.GetIssue():
1719 return
1720
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001721 status = self._GetChangeDetail()['status']
1722 if status in ('MERGED', 'ABANDONED'):
1723 DieWithError('Change %s has been %s, new uploads are not allowed' %
1724 (self.GetIssueURL(),
1725 'submitted' if status == 'MERGED' else 'abandoned'))
1726
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001727 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1728 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1729 # Apparently this check is not very important? Otherwise get_auth_email
1730 # could have been added to other implementations of Authenticator.
1731 cookies_auth = gerrit_util.Authenticator.get()
1732 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001733 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001734
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001735 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001736 if self.GetIssueOwner() == cookies_user:
1737 return
1738 logging.debug('change %s owner is %s, cookies user is %s',
1739 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001740 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001741 # so ask what Gerrit thinks of this user.
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001742 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001743 if details['email'] == self.GetIssueOwner():
1744 return
1745 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001746 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001747 'as %s.\n'
1748 'Uploading may fail due to lack of permissions.' %
1749 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1750 confirm_or_exit(action='upload')
1751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001753 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001754 or CQ status, assuming adherence to a common workflow.
1755
1756 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001757 * 'error' - error from review tool (including deleted issues)
1758 * 'unsent' - no reviewers added
1759 * 'waiting' - waiting for review
1760 * 'reply' - waiting for uploader to reply to review
1761 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001762 * 'dry-run' - dry-running in the CQ
1763 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001764 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001765 """
1766 if not self.GetIssue():
1767 return None
1768
1769 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001770 data = self._GetChangeDetail([
1771 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001772 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001773 return 'error'
1774
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001775 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001776 return 'closed'
1777
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001778 cq_label = data['labels'].get('Commit-Queue', {})
1779 max_cq_vote = 0
1780 for vote in cq_label.get('all', []):
1781 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1782 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001783 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001784 if max_cq_vote == 1:
1785 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001786
Aaron Gable9ab38c62017-04-06 14:36:33 -07001787 if data['labels'].get('Code-Review', {}).get('approved'):
1788 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001789
1790 if not data.get('reviewers', {}).get('REVIEWER', []):
1791 return 'unsent'
1792
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001793 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001794 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001795 while messages:
1796 m = messages.pop()
1797 if m.get('tag', '').startswith('autogenerated:cq:'):
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001798 # Ignore replies from CQ.
1799 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001800 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001801 # Most recent message was by owner.
1802 return 'waiting'
1803 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001804 # Some reply from non-owner.
1805 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001806
1807 # Somehow there are no messages even though there are reviewers.
1808 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001809
1810 def GetMostRecentPatchset(self):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001811 if not self.GetIssue():
1812 return None
1813
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001814 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001815 patchset = data['revisions'][data['current_revision']]['_number']
1816 self.SetPatchset(patchset)
1817 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818
Gavin Make61ccc52020-11-13 00:12:57 +00001819 def GetMostRecentDryRunPatchset(self):
1820 """Get patchsets equivalent to the most recent patchset and return
1821 the patchset with the latest dry run. If none have been dry run, return
1822 the latest patchset."""
1823 if not self.GetIssue():
1824 return None
1825
1826 data = self._GetChangeDetail(['ALL_REVISIONS'])
1827 patchset = data['revisions'][data['current_revision']]['_number']
1828 dry_run = set([int(m['_revision_number'])
1829 for m in data.get('messages', [])
1830 if m.get('tag', '').endswith('dry-run')])
1831
1832 for revision_info in sorted(data.get('revisions', {}).values(),
1833 key=lambda c: c['_number'], reverse=True):
1834 if revision_info['_number'] in dry_run:
1835 patchset = revision_info['_number']
1836 break
1837 if revision_info.get('kind', '') not in \
1838 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
1839 break
1840 self.SetPatchset(patchset)
1841 return patchset
1842
Aaron Gable636b13f2017-07-14 10:42:48 -07001843 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001844 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001845 self.GetGerritHost(), self._GerritChangeIdentifier(),
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001846 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001847
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001848 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001849 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001850 # CURRENT_REVISION is included to get the latest patchset so that
1851 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001852 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001853 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1854 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001855 file_comments = gerrit_util.GetChangeComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001856 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001857 robot_file_comments = gerrit_util.GetChangeRobotComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001858 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001859
1860 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001861 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001862 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001863 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001864 line_comments = file_comments.setdefault(path, [])
1865 line_comments.extend(
1866 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001867
1868 # Build dictionary of file comments for easy access and sorting later.
1869 # {author+date: {path: {patchset: {line: url+message}}}}
1870 comments = collections.defaultdict(
1871 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00001872
1873 server = self.GetCodereviewServer()
1874 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
1875 # /c/ is automatically added by short URL server.
1876 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
1877 self.GetIssue())
1878 else:
1879 url_prefix = '%s/c/%s' % (server, self.GetIssue())
1880
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001881 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001882 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001883 tag = comment.get('tag', '')
1884 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001885 continue
1886 key = (comment['author']['email'], comment['updated'])
1887 if comment.get('side', 'REVISION') == 'PARENT':
1888 patchset = 'Base'
1889 else:
1890 patchset = 'PS%d' % comment['patch_set']
1891 line = comment.get('line', 0)
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00001892 url = ('%s/%s/%s#%s%s' %
1893 (url_prefix, comment['patch_set'], path,
1894 'b' if comment.get('side') == 'PARENT' else '',
1895 str(line) if line else ''))
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001896 comments[key][path][patchset][line] = (url, comment['message'])
1897
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001898 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001899 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001900 summary = self._BuildCommentSummary(msg, comments, readable)
1901 if summary:
1902 summaries.append(summary)
1903 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001904
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001905 @staticmethod
1906 def _BuildCommentSummary(msg, comments, readable):
1907 key = (msg['author']['email'], msg['date'])
1908 # Don't bother showing autogenerated messages that don't have associated
1909 # file or line comments. this will filter out most autogenerated
1910 # messages, but will keep robot comments like those from Tricium.
1911 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1912 if is_autogenerated and not comments.get(key):
1913 return None
1914 message = msg['message']
1915 # Gerrit spits out nanoseconds.
1916 assert len(msg['date'].split('.')[-1]) == 9
1917 date = datetime.datetime.strptime(msg['date'][:-3],
1918 '%Y-%m-%d %H:%M:%S.%f')
1919 if key in comments:
1920 message += '\n'
1921 for path, patchsets in sorted(comments.get(key, {}).items()):
1922 if readable:
1923 message += '\n%s' % path
1924 for patchset, lines in sorted(patchsets.items()):
1925 for line, (url, content) in sorted(lines.items()):
1926 if line:
1927 line_str = 'Line %d' % line
1928 path_str = '%s:%d:' % (path, line)
1929 else:
1930 line_str = 'File comment'
1931 path_str = '%s:0:' % path
1932 if readable:
1933 message += '\n %s, %s: %s' % (patchset, line_str, url)
1934 message += '\n %s\n' % content
1935 else:
1936 message += '\n%s ' % path_str
1937 message += '\n%s\n' % content
1938
1939 return _CommentSummary(
1940 date=date,
1941 message=message,
1942 sender=msg['author']['email'],
1943 autogenerated=is_autogenerated,
1944 # These could be inferred from the text messages and correlated with
1945 # Code-Review label maximum, however this is not reliable.
1946 # Leaving as is until the need arises.
1947 approval=False,
1948 disapproval=False,
1949 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001950
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001952 gerrit_util.AbandonChange(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001953 self.GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001955 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001956 gerrit_util.SubmitChange(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001957 self.GetGerritHost(), self._GerritChangeIdentifier(),
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001958 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001960 def _GetChangeDetail(self, options=None):
1961 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001962 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001963 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001964
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001965 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001966 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001967 options.append('CURRENT_COMMIT')
1968
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001969 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001970 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001971 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001972
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001973 for cached_options_set, data in self._detail_cache.get(cache_key, []):
1974 # Assumption: data fetched before with extra options is suitable
1975 # for return for a smaller set of options.
1976 # For example, if we cached data for
1977 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
1978 # and request is for options=[CURRENT_REVISION],
1979 # THEN we can return prior cached data.
1980 if options_set.issubset(cached_options_set):
1981 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001982
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001983 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001984 data = gerrit_util.GetChangeDetail(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001985 self.GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001986 except gerrit_util.GerritError as e:
1987 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001988 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001989 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001990
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001991 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07001992 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001993
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00001994 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001995 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001996 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001997 data = gerrit_util.GetChangeCommit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001998 self.GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001999 except gerrit_util.GerritError as e:
2000 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002001 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002002 raise
agable32978d92016-11-01 12:55:02 -07002003 return data
2004
Karen Qian40c19422019-03-13 21:28:29 +00002005 def _IsCqConfigured(self):
2006 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00002007 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00002008
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002009 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002010 if git_common.is_dirty_git_tree('land'):
2011 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002012
tandriid60367b2016-06-22 05:25:12 -07002013 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002014 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002015 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002016 'which can test and land changes for you. '
2017 'Are you sure you wish to bypass it?\n',
2018 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002019 differs = True
tandriic4344b52016-08-29 06:04:54 -07002020 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002021 # Note: git diff outputs nothing if there is no diff.
2022 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002023 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002024 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002025 if detail['current_revision'] == last_upload:
2026 differs = False
2027 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002028 print('WARNING: Local branch contents differ from latest uploaded '
2029 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002030 if differs:
2031 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002032 confirm_or_exit(
2033 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2034 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002035 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002036 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00002037 upstream = self.GetCommonAncestorWithUpstream()
2038 if self.GetIssue():
2039 description = self.FetchDescription()
2040 else:
Edward Lemura12175c2020-03-09 16:58:26 +00002041 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00002042 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002043 committing=True,
2044 may_prompt=not force,
2045 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00002046 parallel=parallel,
2047 upstream=upstream,
2048 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00002049 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002050 resultdb=resultdb,
2051 realm=realm)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002052
2053 self.SubmitIssue(wait_for_merge=True)
2054 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002055 links = self._GetChangeCommit().get('web_links', [])
2056 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002057 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002058 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002059 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002060 return 0
2061
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002062 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2063 newbranch):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002064 assert parsed_issue_arg.valid
2065
Edward Lemur125d60a2019-09-13 18:25:41 +00002066 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002067
2068 if parsed_issue_arg.hostname:
2069 self._gerrit_host = parsed_issue_arg.hostname
2070 self._gerrit_server = 'https://%s' % self._gerrit_host
2071
tandriic2405f52016-10-10 08:13:15 -07002072 try:
2073 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002074 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002075 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002076
2077 if not parsed_issue_arg.patchset:
2078 # Use current revision by default.
2079 revision_info = detail['revisions'][detail['current_revision']]
2080 patchset = int(revision_info['_number'])
2081 else:
2082 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002083 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002084 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2085 break
2086 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002087 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002088 (parsed_issue_arg.patchset, self.GetIssue()))
2089
Edward Lemur125d60a2019-09-13 18:25:41 +00002090 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002091 if remote_url.endswith('.git'):
2092 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002093 remote_url = remote_url.rstrip('/')
2094
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002095 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002096 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002097
2098 if remote_url != fetch_info['url']:
2099 DieWithError('Trying to patch a change from %s but this repo appears '
2100 'to be %s.' % (fetch_info['url'], remote_url))
2101
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002102 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002103
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002104 # If we have created a new branch then do the "set issue" immediately in
2105 # case the cherry-pick fails, which happens when resolving conflicts.
2106 if newbranch:
2107 self.SetIssue(parsed_issue_arg.issue)
2108
Aaron Gable62619a32017-06-16 08:22:09 -07002109 if force:
2110 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2111 print('Checked out commit for change %i patchset %i locally' %
2112 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002113 elif nocommit:
2114 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2115 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002116 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002117 RunGit(['cherry-pick', 'FETCH_HEAD'])
2118 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002119 (parsed_issue_arg.issue, patchset))
2120 print('Note: this created a local commit which does not have '
2121 'the same hash as the one uploaded for review. This will make '
2122 'uploading changes based on top of this branch difficult.\n'
2123 'If you want to do that, use "git cl patch --force" instead.')
2124
Stefan Zagerd08043c2017-10-12 12:07:02 -07002125 if self.GetBranch():
2126 self.SetIssue(parsed_issue_arg.issue)
2127 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00002128 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Stefan Zagerd08043c2017-10-12 12:07:02 -07002129 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2130 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2131 else:
2132 print('WARNING: You are in detached HEAD state.\n'
2133 'The patch has been applied to your checkout, but you will not be '
2134 'able to upload a new patch set to the gerrit issue.\n'
2135 'Try using the \'-b\' option if you would like to work on a '
2136 'branch and/or upload a new patch set.')
2137
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002138 return 0
2139
tandrii16e0b4e2016-06-07 10:34:28 -07002140 def _GerritCommitMsgHookCheck(self, offer_removal):
2141 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2142 if not os.path.exists(hook):
2143 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002144 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2145 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002146 data = gclient_utils.FileRead(hook)
2147 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2148 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002149 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002150 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002151 'and may interfere with it in subtle ways.\n'
2152 'We recommend you remove the commit-msg hook.')
2153 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002154 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002155 gclient_utils.rm_file_or_tree(hook)
2156 print('Gerrit commit-msg hook removed.')
2157 else:
2158 print('OK, will keep Gerrit commit-msg hook in place.')
2159
Edward Lemur1b52d872019-05-09 21:12:12 +00002160 def _CleanUpOldTraces(self):
2161 """Keep only the last |MAX_TRACES| traces."""
2162 try:
2163 traces = sorted([
2164 os.path.join(TRACES_DIR, f)
2165 for f in os.listdir(TRACES_DIR)
2166 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2167 and not f.startswith('tmp'))
2168 ])
2169 traces_to_delete = traces[:-MAX_TRACES]
2170 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002171 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002172 except OSError:
2173 print('WARNING: Failed to remove old git traces from\n'
2174 ' %s'
2175 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002176
Edward Lemur5737f022019-05-17 01:24:00 +00002177 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002178 """Zip and write the git push traces stored in traces_dir."""
2179 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002180 traces_zip = trace_name + '-traces'
2181 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002182 # Create a temporary dir to store git config and gitcookies in. It will be
2183 # compressed and stored next to the traces.
2184 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002185 git_info_zip = trace_name + '-git-info'
2186
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002187 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002188
Edward Lemur1b52d872019-05-09 21:12:12 +00002189 git_push_metadata['trace_name'] = trace_name
2190 gclient_utils.FileWrite(
2191 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2192
2193 # Keep only the first 6 characters of the git hashes on the packet
2194 # trace. This greatly decreases size after compression.
2195 packet_traces = os.path.join(traces_dir, 'trace-packet')
2196 if os.path.isfile(packet_traces):
2197 contents = gclient_utils.FileRead(packet_traces)
2198 gclient_utils.FileWrite(
2199 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2200 shutil.make_archive(traces_zip, 'zip', traces_dir)
2201
2202 # Collect and compress the git config and gitcookies.
2203 git_config = RunGit(['config', '-l'])
2204 gclient_utils.FileWrite(
2205 os.path.join(git_info_dir, 'git-config'),
2206 git_config)
2207
2208 cookie_auth = gerrit_util.Authenticator.get()
2209 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2210 gitcookies_path = cookie_auth.get_gitcookies_path()
2211 if os.path.isfile(gitcookies_path):
2212 gitcookies = gclient_utils.FileRead(gitcookies_path)
2213 gclient_utils.FileWrite(
2214 os.path.join(git_info_dir, 'gitcookies'),
2215 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2216 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2217
Edward Lemur1b52d872019-05-09 21:12:12 +00002218 gclient_utils.rmtree(git_info_dir)
2219
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002220 def _RunGitPushWithTraces(self,
2221 refspec,
2222 refspec_opts,
2223 git_push_metadata,
2224 git_push_options=None):
Edward Lemur1b52d872019-05-09 21:12:12 +00002225 """Run git push and collect the traces resulting from the execution."""
2226 # Create a temporary directory to store traces in. Traces will be compressed
2227 # and stored in a 'traces' dir inside depot_tools.
2228 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002229 trace_name = os.path.join(
2230 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002231
2232 env = os.environ.copy()
2233 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2234 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002235 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002236 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2237 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2238 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2239
2240 try:
2241 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002242 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002243 before_push = time_time()
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002244 push_cmd = ['git', 'push', remote_url, refspec]
2245 if git_push_options:
2246 for opt in git_push_options:
2247 push_cmd.extend(['-o', opt])
2248
Edward Lemur0f58ae42019-04-30 17:24:12 +00002249 push_stdout = gclient_utils.CheckCallAndFilter(
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002250 push_cmd,
Edward Lemur0f58ae42019-04-30 17:24:12 +00002251 env=env,
2252 print_stdout=True,
2253 # Flush after every line: useful for seeing progress when running as
2254 # recipe.
2255 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002256 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002257 except subprocess2.CalledProcessError as e:
2258 push_returncode = e.returncode
Josip Sokcevic740825e2021-05-12 18:28:34 +00002259 if 'blocked keyword' in str(e.stdout):
2260 raise GitPushError(
2261 'Failed to create a change, very likely due to blocked keyword. '
2262 'Please examine output above for the reason of the failure.\n'
2263 'If this is a false positive, you can try to bypass blocked '
2264 'keyword by using push option '
2265 '-o uploadvalidator~skip, e.g.:\n'
2266 'git cl upload -o uploadvalidator~skip\n\n'
2267 'If git-cl is not working correctly, file a bug under the '
2268 'Infra>SDK component.')
2269
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002270 raise GitPushError(
2271 'Failed to create a change. Please examine output above for the '
2272 'reason of the failure.\n'
Josip Sokcevic7386a1e2021-02-12 19:00:34 +00002273 'For emergencies, Googlers can escalate to '
2274 'go/gob-support or go/notify#gob\n'
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002275 'Hint: run command below to diagnose common Git/Gerrit '
2276 'credential problems:\n'
2277 ' git cl creds-check\n'
2278 '\n'
2279 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2280 'component including the files below.\n'
2281 'Review the files before upload, since they might contain sensitive '
2282 'information.\n'
2283 'Set the Restrict-View-Google label so that they are not publicly '
2284 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
Edward Lemur0f58ae42019-04-30 17:24:12 +00002285 finally:
2286 execution_time = time_time() - before_push
2287 metrics.collector.add_repeated('sub_commands', {
2288 'command': 'git push',
2289 'execution_time': execution_time,
2290 'exit_code': push_returncode,
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002291 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
Edward Lemur0f58ae42019-04-30 17:24:12 +00002292 })
2293
Edward Lemur1b52d872019-05-09 21:12:12 +00002294 git_push_metadata['execution_time'] = execution_time
2295 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002296 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002297
Edward Lemur1b52d872019-05-09 21:12:12 +00002298 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002299 gclient_utils.rmtree(traces_dir)
2300
2301 return push_stdout
2302
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002303 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
2304 change_desc):
2305 """Upload the current branch to Gerrit, retry if new remote HEAD is
2306 found. options and change_desc may be mutated."""
Josip Sokcevicb631a882021-01-06 18:18:10 +00002307 remote, remote_branch = self.GetRemoteBranch()
2308 branch = GetTargetRef(remote, remote_branch, options.target_branch)
2309
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002310 try:
2311 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002312 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002313 except GitPushError as e:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002314 # Repository might be in the middle of transition to main branch as
2315 # default, and uploads to old default might be blocked.
2316 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002317 DieWithError(str(e), change_desc)
2318
Josip Sokcevicb631a882021-01-06 18:18:10 +00002319 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
2320 self.GetGerritProject())
2321 if project_head == branch:
2322 DieWithError(str(e), change_desc)
2323 branch = project_head
2324
2325 print("WARNING: Fetching remote state and retrying upload to default "
2326 "branch...")
2327 RunGit(['fetch', '--prune', remote])
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002328 options.edit_description = False
2329 options.force = True
2330 try:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002331 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
2332 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002333 except GitPushError as e:
2334 DieWithError(str(e), change_desc)
2335
2336 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002337 change_desc, branch):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002338 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002339 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002340 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002341 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002342 # User requested to change description
2343 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002344 change_desc.prompt()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002345 change_id = self._GetChangeDetail()['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002346 change_desc.ensure_change_id(change_id)
Aaron Gableb56ad332017-01-06 15:24:31 -08002347 else: # if not self.GetIssue()
Gavin Mak68e6cf32021-01-25 18:24:08 +00002348 if not options.force and not options.message_file:
Anthony Polito8b955342019-09-24 19:01:36 +00002349 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002350 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002351 if len(change_ids) == 1:
2352 change_id = change_ids[0]
2353 else:
2354 change_id = GenerateGerritChangeId(change_desc.description)
2355 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002356
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002357 if options.preserve_tryjobs:
2358 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002359
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002360 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Edward Lemur5a644f82020-03-18 16:44:57 +00002361 parent = self._ComputeParent(
2362 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002363 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002364 with gclient_utils.temporary_file() as desc_tempfile:
2365 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2366 ref_to_push = RunGit(
2367 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002368 else: # if not options.squash
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002369 if options.no_add_changeid:
2370 pass
2371 else: # adding Change-Ids is okay.
2372 if not git_footers.get_footer_change_id(change_desc.description):
2373 DownloadGerritHook(False)
2374 change_desc.set_description(
2375 self._AddChangeIdToCommitMessage(change_desc.description,
2376 git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002377 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002378 # For no-squash mode, we assume the remote called "origin" is the one we
2379 # want. It is not worthwhile to support different workflows for
2380 # no-squash mode.
2381 parent = 'origin/%s' % branch
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002382 # attempt to extract the changeid from the current description
2383 # fail informatively if not possible.
2384 change_id_candidates = git_footers.get_footer_change_id(
2385 change_desc.description)
2386 if not change_id_candidates:
2387 DieWithError("Unable to extract change-id from message.")
2388 change_id = change_id_candidates[0]
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002389
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002390 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002391 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2392 ref_to_push)]).splitlines()
2393 if len(commits) > 1:
2394 print('WARNING: This will upload %d commits. Run the following command '
2395 'to see which commits will be uploaded: ' % len(commits))
2396 print('git log %s..%s' % (parent, ref_to_push))
2397 print('You can also use `git squash-branch` to squash these into a '
2398 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002399 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002400
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002401 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002402 cc = []
2403 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2404 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2405 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002406 cc = self.GetCCList().split(',')
Gavin Makb1c08f62021-04-01 18:05:58 +00002407 if len(cc) > 100:
2408 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2409 'process/lsc/lsc_workflow.md')
2410 print('WARNING: This will auto-CC %s users.' % len(cc))
2411 print('LSC may be more appropriate: %s' % lsc)
2412 print('You can also use the --no-autocc flag to disable auto-CC.')
2413 confirm_or_exit(action='continue')
Edward Lemur4508b422019-10-03 21:56:35 +00002414 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002415 if options.cc:
2416 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002417 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002418 if change_desc.get_cced():
2419 cc.extend(change_desc.get_cced())
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002420 if self.GetGerritHost() == 'chromium-review.googlesource.com':
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002421 valid_accounts = set(reviewers + cc)
2422 # TODO(crbug/877717): relax this for all hosts.
2423 else:
2424 valid_accounts = gerrit_util.ValidAccounts(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002425 self.GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002426 logging.info('accounts %s are recognized, %s invalid',
2427 sorted(valid_accounts),
2428 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002429
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002430 # Extra options that can be specified at push time. Doc:
2431 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002432 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002433
Aaron Gable844cf292017-06-28 11:32:59 -07002434 # By default, new changes are started in WIP mode, and subsequent patchsets
2435 # don't send email. At any time, passing --send-mail will mark the change
2436 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002437 if options.send_mail:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002438 refspec_opts.append('ready')
2439 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002440 elif not self.GetIssue() and options.squash:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002441 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002442 else:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002443 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002444
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002445 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002446 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002447
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002448 # Set options.title in case user was prompted in _GetTitleForUpload and
2449 # _CMDUploadChange needs to be called again.
2450 options.title = self._GetTitleForUpload(options)
2451 if options.title:
Nick Carter8692b182017-11-06 16:30:38 -08002452 # Punctuation and whitespace in |title| must be percent-encoded.
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002453 refspec_opts.append(
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002454 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002455
agablec6787972016-09-09 16:13:34 -07002456 if options.private:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002457 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002458
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002459 for r in sorted(reviewers):
2460 if r in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002461 refspec_opts.append('r=%s' % r)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002462 reviewers.remove(r)
2463 else:
2464 # TODO(tandrii): this should probably be a hard failure.
2465 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2466 % r)
2467 for c in sorted(cc):
2468 # refspec option will be rejected if cc doesn't correspond to an
2469 # account, even though REST call to add such arbitrary cc may succeed.
2470 if c in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002471 refspec_opts.append('cc=%s' % c)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002472 cc.remove(c)
2473
rmistry9eadede2016-09-19 11:22:43 -07002474 if options.topic:
2475 # Documentation on Gerrit topics is here:
2476 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002477 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002478
Edward Lemur687ca902018-12-05 02:30:30 +00002479 if options.enable_auto_submit:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002480 refspec_opts.append('l=Auto-Submit+1')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00002481 if options.set_bot_commit:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002482 refspec_opts.append('l=Bot-Commit+1')
Edward Lemur687ca902018-12-05 02:30:30 +00002483 if options.use_commit_queue:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002484 refspec_opts.append('l=Commit-Queue+2')
Edward Lemur687ca902018-12-05 02:30:30 +00002485 elif options.cq_dry_run:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002486 refspec_opts.append('l=Commit-Queue+1')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00002487 elif options.cq_quick_run:
2488 refspec_opts.append('l=Commit-Queue+1')
2489 refspec_opts.append('l=Quick-Run+1')
Edward Lemur687ca902018-12-05 02:30:30 +00002490
2491 if change_desc.get_reviewers(tbr_only=True):
2492 score = gerrit_util.GetCodeReviewTbrScore(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002493 self.GetGerritHost(),
2494 self.GetGerritProject())
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002495 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002496
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002497 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002498 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002499 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002500 hashtags.update(change_desc.get_hash_tags())
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002501 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2502
2503 refspec_suffix = ''
2504 if refspec_opts:
2505 refspec_suffix = '%' + ','.join(refspec_opts)
2506 assert ' ' not in refspec_suffix, (
2507 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2508 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002509
Edward Lemur1b52d872019-05-09 21:12:12 +00002510 git_push_metadata = {
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002511 'gerrit_host': self.GetGerritHost(),
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002512 'title': options.title or '<untitled>',
Edward Lemur1b52d872019-05-09 21:12:12 +00002513 'change_id': change_id,
2514 'description': change_desc.description,
2515 }
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002516
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002517 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002518 git_push_metadata,
2519 options.push_options)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002520
2521 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002522 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002523 change_numbers = [m.group(1)
2524 for m in map(regex.match, push_stdout.splitlines())
2525 if m]
2526 if len(change_numbers) != 1:
2527 DieWithError(
2528 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002529 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002530 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002531 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002532
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002533 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002534 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002535 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002536 gerrit_util.AddReviewers(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002537 self.GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002538 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002539 reviewers, cc,
2540 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002541
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542 return 0
2543
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002544 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2545 change_desc):
2546 """Computes parent of the generated commit to be uploaded to Gerrit.
2547
2548 Returns revision or a ref name.
2549 """
2550 if custom_cl_base:
2551 # Try to avoid creating additional unintended CLs when uploading, unless
2552 # user wants to take this risk.
2553 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2554 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2555 local_ref_of_target_remote])
2556 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002557 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002558 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2559 'If you proceed with upload, more than 1 CL may be created by '
2560 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2561 'If you are certain that specified base `%s` has already been '
2562 'uploaded to Gerrit as another CL, you may proceed.\n' %
2563 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2564 if not force:
2565 confirm_or_exit(
2566 'Do you take responsibility for cleaning up potential mess '
2567 'resulting from proceeding with upload?',
2568 action='upload')
2569 return custom_cl_base
2570
Aaron Gablef97e33d2017-03-30 15:44:27 -07002571 if remote != '.':
2572 return self.GetCommonAncestorWithUpstream()
2573
2574 # If our upstream branch is local, we base our squashed commit on its
2575 # squashed version.
2576 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2577
Aaron Gablef97e33d2017-03-30 15:44:27 -07002578 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002579 return self.GetCommonAncestorWithUpstream()
Glen Robertson7d98e222020-08-27 17:53:11 +00002580 if upstream_branch_name == 'main':
2581 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002582
2583 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002584 # TODO(tandrii): consider checking parent change in Gerrit and using its
2585 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2586 # the tree hash of the parent branch. The upside is less likely bogus
2587 # requests to reupload parent change just because it's uploadhash is
2588 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Edward Lesmesa680c232020-03-31 18:26:44 +00002589 parent = scm.GIT.GetBranchConfig(
2590 settings.GetRoot(), upstream_branch_name, 'gerritsquashhash')
Aaron Gablef97e33d2017-03-30 15:44:27 -07002591 # Verify that the upstream branch has been uploaded too, otherwise
2592 # Gerrit will create additional CLs when uploading.
2593 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2594 RunGitSilent(['rev-parse', parent + ':'])):
2595 DieWithError(
2596 '\nUpload upstream branch %s first.\n'
2597 'It is likely that this branch has been rebased since its last '
2598 'upload, so you just need to upload it again.\n'
2599 '(If you uploaded it with --no-squash, then branch dependencies '
2600 'are not supported, and you should reupload with --squash.)'
2601 % upstream_branch_name,
2602 change_desc)
2603 return parent
2604
Edward Lemura12175c2020-03-09 16:58:26 +00002605 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002606 """Re-commits using the current message, assumes the commit hook is in
2607 place.
2608 """
Edward Lemura12175c2020-03-09 16:58:26 +00002609 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002610 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002611 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002612 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002613 return new_log_desc
2614 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002615 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002616
tandriie113dfd2016-10-11 10:20:12 -07002617 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002618 try:
2619 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002620 except GerritChangeNotExists:
2621 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002622
2623 if data['status'] in ('ABANDONED', 'MERGED'):
2624 return 'CL %s is closed' % self.GetIssue()
2625
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002626 def GetGerritChange(self, patchset=None):
2627 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002628 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002629 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002630 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002631 data = self._GetChangeDetail(['ALL_REVISIONS'])
2632
2633 assert host and issue and patchset, 'CL must be uploaded first'
2634
2635 has_patchset = any(
2636 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002637 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002638 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002639 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002640 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002641
tandrii8c5a3532016-11-04 07:52:02 -07002642 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002643 'host': host,
2644 'change': issue,
2645 'project': data['project'],
2646 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002647 }
tandriie113dfd2016-10-11 10:20:12 -07002648
tandriide281ae2016-10-12 06:02:30 -07002649 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002650 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002651
Edward Lemur707d70b2018-02-07 00:50:14 +01002652 def GetReviewers(self):
2653 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002654 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002655
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002656
Lei Zhang8a0efc12020-08-05 19:58:45 +00002657def _get_bug_line_values(default_project_prefix, bugs):
2658 """Given default_project_prefix and comma separated list of bugs, yields bug
2659 line values.
tandriif9aefb72016-07-01 09:06:51 -07002660
2661 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00002662 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07002663 * string, which is left as is.
2664
2665 This function may produce more than one line, because bugdroid expects one
2666 project per line.
2667
Lei Zhang8a0efc12020-08-05 19:58:45 +00002668 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07002669 ['v8:123', 'chromium:789']
2670 """
2671 default_bugs = []
2672 others = []
2673 for bug in bugs.split(','):
2674 bug = bug.strip()
2675 if bug:
2676 try:
2677 default_bugs.append(int(bug))
2678 except ValueError:
2679 others.append(bug)
2680
2681 if default_bugs:
2682 default_bugs = ','.join(map(str, default_bugs))
Lei Zhang8a0efc12020-08-05 19:58:45 +00002683 if default_project_prefix:
2684 if not default_project_prefix.endswith(':'):
2685 default_project_prefix += ':'
2686 yield '%s%s' % (default_project_prefix, default_bugs)
tandriif9aefb72016-07-01 09:06:51 -07002687 else:
2688 yield default_bugs
2689 for other in sorted(others):
2690 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2691 yield other
2692
2693
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002694class ChangeDescription(object):
2695 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002696 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002697 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002698 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002699 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002700 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002701 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2702 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002703 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002704 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002705
Dan Beamd8b04ca2019-10-10 21:23:26 +00002706 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002707 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002708 if bug:
2709 regexp = re.compile(self.BUG_LINE)
2710 prefix = settings.GetBugPrefix()
2711 if not any((regexp.match(line) for line in self._description_lines)):
2712 values = list(_get_bug_line_values(prefix, bug))
2713 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002714 if fixed:
2715 regexp = re.compile(self.FIXED_LINE)
2716 prefix = settings.GetBugPrefix()
2717 if not any((regexp.match(line) for line in self._description_lines)):
2718 values = list(_get_bug_line_values(prefix, fixed))
2719 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002720
agable@chromium.org42c20792013-09-12 17:34:49 +00002721 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002722 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002723 return '\n'.join(self._description_lines)
2724
2725 def set_description(self, desc):
2726 if isinstance(desc, basestring):
2727 lines = desc.splitlines()
2728 else:
2729 lines = [line.rstrip() for line in desc]
2730 while lines and not lines[0]:
2731 lines.pop(0)
2732 while lines and not lines[-1]:
2733 lines.pop(-1)
2734 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002735
Edward Lemur5a644f82020-03-18 16:44:57 +00002736 def ensure_change_id(self, change_id):
2737 description = self.description
2738 footer_change_ids = git_footers.get_footer_change_id(description)
2739 # Make sure that the Change-Id in the description matches the given one.
2740 if footer_change_ids != [change_id]:
2741 if footer_change_ids:
2742 # Remove any existing Change-Id footers since they don't match the
2743 # expected change_id footer.
2744 description = git_footers.remove_footer(description, 'Change-Id')
2745 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
2746 'if you want to set a new one.')
2747 # Add the expected Change-Id footer.
2748 description = git_footers.add_footer_change_id(description, change_id)
2749 self.set_description(description)
2750
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00002751 def update_reviewers(self, reviewers, tbrs):
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002752 """Rewrites the R=/TBR= line(s) as a single line each.
2753
2754 Args:
2755 reviewers (list(str)) - list of additional emails to use for reviewers.
2756 tbrs (list(str)) - list of additional emails to use for TBRs.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002757 """
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00002758 if not reviewers and not tbrs:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002759 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002760
2761 reviewers = set(reviewers)
2762 tbrs = set(tbrs)
2763 LOOKUP = {
2764 'TBR': tbrs,
2765 'R': reviewers,
2766 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002767
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002768 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002769 regexp = re.compile(self.R_LINE)
2770 matches = [regexp.match(line) for line in self._description_lines]
2771 new_desc = [l for i, l in enumerate(self._description_lines)
2772 if not matches[i]]
2773 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002774
agable@chromium.org42c20792013-09-12 17:34:49 +00002775 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002776
2777 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002778 for match in matches:
2779 if not match:
2780 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002781 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2782
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002783 # If any folks ended up in both groups, remove them from tbrs.
2784 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002785
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002786 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2787 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002788
2789 # Put the new lines in the description where the old first R= line was.
2790 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2791 if 0 <= line_loc < len(self._description_lines):
2792 if new_tbr_line:
2793 self._description_lines.insert(line_loc, new_tbr_line)
2794 if new_r_line:
2795 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002796 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002797 if new_r_line:
2798 self.append_footer(new_r_line)
2799 if new_tbr_line:
2800 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002801
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002802 def set_preserve_tryjobs(self):
2803 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2804 footers = git_footers.parse_footers(self.description)
2805 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2806 if v.lower() == 'true':
2807 return
2808 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2809
Anthony Polito8b955342019-09-24 19:01:36 +00002810 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002811 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002812 self.set_description([
2813 '# Enter a description of the change.',
2814 '# This will be displayed on the codereview site.',
2815 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002816 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002817 '--------------------',
2818 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002819 bug_regexp = re.compile(self.BUG_LINE)
2820 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002821 prefix = settings.GetBugPrefix()
Sigurd Schneider8630bb12020-11-11 14:02:49 +00002822 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00002823
Dan Beamd8b04ca2019-10-10 21:23:26 +00002824 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002825 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002826
Bruce Dawsonfc487042020-10-27 19:11:37 +00002827 print('Waiting for editor...')
agable@chromium.org42c20792013-09-12 17:34:49 +00002828 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002829 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002830 if not content:
2831 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002832 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002833
Bruce Dawson2377b012018-01-11 16:46:49 -08002834 # Strip off comments and default inserted "Bug:" line.
2835 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002836 (line.startswith('#') or
2837 line.rstrip() == "Bug:" or
2838 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002839 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002840 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002841 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002842
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002843 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002844 """Adds a footer line to the description.
2845
2846 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2847 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2848 that Gerrit footers are always at the end.
2849 """
2850 parsed_footer_line = git_footers.parse_footer(line)
2851 if parsed_footer_line:
2852 # Line is a gerrit footer in the form: Footer-Key: any value.
2853 # Thus, must be appended observing Gerrit footer rules.
2854 self.set_description(
2855 git_footers.add_footer(self.description,
2856 key=parsed_footer_line[0],
2857 value=parsed_footer_line[1]))
2858 return
2859
2860 if not self._description_lines:
2861 self._description_lines.append(line)
2862 return
2863
2864 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2865 if gerrit_footers:
2866 # git_footers.split_footers ensures that there is an empty line before
2867 # actual (gerrit) footers, if any. We have to keep it that way.
2868 assert top_lines and top_lines[-1] == ''
2869 top_lines, separator = top_lines[:-1], top_lines[-1:]
2870 else:
2871 separator = [] # No need for separator if there are no gerrit_footers.
2872
2873 prev_line = top_lines[-1] if top_lines else ''
2874 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2875 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2876 top_lines.append('')
2877 top_lines.append(line)
2878 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002879
tandrii99a72f22016-08-17 14:33:24 -07002880 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002881 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002882 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002883 reviewers = [match.group(2).strip()
2884 for match in matches
2885 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002886 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002887
bradnelsond975b302016-10-23 12:20:23 -07002888 def get_cced(self):
2889 """Retrieves the list of reviewers."""
2890 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2891 cced = [match.group(2).strip() for match in matches if match]
2892 return cleanup_list(cced)
2893
Nodir Turakulov23b82142017-11-16 11:04:25 -08002894 def get_hash_tags(self):
2895 """Extracts and sanitizes a list of Gerrit hashtags."""
2896 subject = (self._description_lines or ('',))[0]
2897 subject = re.sub(
2898 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2899
2900 tags = []
2901 start = 0
2902 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2903 while True:
2904 m = bracket_exp.match(subject, start)
2905 if not m:
2906 break
2907 tags.append(self.sanitize_hash_tag(m.group(1)))
2908 start = m.end()
2909
2910 if not tags:
2911 # Try "Tag: " prefix.
2912 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
2913 if m:
2914 tags.append(self.sanitize_hash_tag(m.group(1)))
2915 return tags
2916
2917 @classmethod
2918 def sanitize_hash_tag(cls, tag):
2919 """Returns a sanitized Gerrit hash tag.
2920
2921 A sanitized hashtag can be used as a git push refspec parameter value.
2922 """
2923 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
2924
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002926def FindCodereviewSettingsFile(filename='codereview.settings'):
2927 """Finds the given file starting in the cwd and going up.
2928
2929 Only looks up to the top of the repository unless an
2930 'inherit-review-settings-ok' file exists in the root of the repository.
2931 """
2932 inherit_ok_file = 'inherit-review-settings-ok'
2933 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002934 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2936 root = '/'
2937 while True:
2938 if filename in os.listdir(cwd):
2939 if os.path.isfile(os.path.join(cwd, filename)):
2940 return open(os.path.join(cwd, filename))
2941 if cwd == root:
2942 break
2943 cwd = os.path.dirname(cwd)
2944
2945
2946def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002947 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002948 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950 def SetProperty(name, setting, unset_error_ok=False):
2951 fullname = 'rietveld.' + name
2952 if setting in keyvals:
2953 RunGit(['config', fullname, keyvals[setting]])
2954 else:
2955 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2956
tandrii48df5812016-10-17 03:55:37 -07002957 if not keyvals.get('GERRIT_HOST', False):
2958 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002959 # Only server setting is required. Other settings can be absent.
2960 # In that case, we ignore errors raised during option deletion attempt.
2961 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
2962 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2963 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002964 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002965 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
2966 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002967 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2968 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00002969 SetProperty(
2970 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
Dirk Pranke6f0df682021-06-25 00:42:33 +00002971 SetProperty('use-python3', 'USE_PYTHON3', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002973 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002974 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002975
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002976 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00002977 RunGit(['config', 'gerrit.squash-uploads',
2978 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002979
tandrii@chromium.org28253532016-04-14 13:46:56 +00002980 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002981 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002982 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002984 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002985 # should be of the form
2986 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2987 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2989 keyvals['ORIGIN_URL_CONFIG']])
2990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002992def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002993 """Downloads a network object to a local file, like urllib.urlretrieve.
2994
2995 This is necessary because urllib is broken for SSL connections via a proxy.
2996 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002997 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00002998 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002999
3000
ukai@chromium.org712d6102013-11-27 00:52:58 +00003001def hasSheBang(fname):
3002 """Checks fname is a #! script."""
3003 with open(fname) as f:
3004 return f.read(2).startswith('#!')
3005
3006
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003007def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003008 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003009
3010 Args:
3011 force: True to update hooks. False to install hooks if not present.
3012 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00003013 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003014 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3015 if not os.access(dst, os.X_OK):
3016 if os.path.exists(dst):
3017 if not force:
3018 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003019 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003020 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003021 if not hasSheBang(dst):
3022 DieWithError('Not a script: %s\n'
3023 'You need to download from\n%s\n'
3024 'into .git/hooks/commit-msg and '
3025 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003026 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3027 except Exception:
3028 if os.path.exists(dst):
3029 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003030 DieWithError('\nFailed to download hooks.\n'
3031 'You need to download from\n%s\n'
3032 'into .git/hooks/commit-msg and '
3033 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003034
3035
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003036class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003037 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003038
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003039 _GOOGLESOURCE = 'googlesource.com'
3040
3041 def __init__(self):
3042 # Cached list of [host, identity, source], where source is either
3043 # .gitcookies or .netrc.
3044 self._all_hosts = None
3045
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003046 def ensure_configured_gitcookies(self):
3047 """Runs checks and suggests fixes to make git use .gitcookies from default
3048 path."""
3049 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3050 configured_path = RunGitSilent(
3051 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003052 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003053 if configured_path:
3054 self._ensure_default_gitcookies_path(configured_path, default)
3055 else:
3056 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003057
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003058 @staticmethod
3059 def _ensure_default_gitcookies_path(configured_path, default_path):
3060 assert configured_path
3061 if configured_path == default_path:
3062 print('git is already configured to use your .gitcookies from %s' %
3063 configured_path)
3064 return
3065
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003066 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003067 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3068 (configured_path, default_path))
3069
3070 if not os.path.exists(configured_path):
3071 print('However, your configured .gitcookies file is missing.')
3072 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3073 action='reconfigure')
3074 RunGit(['config', '--global', 'http.cookiefile', default_path])
3075 return
3076
3077 if os.path.exists(default_path):
3078 print('WARNING: default .gitcookies file already exists %s' %
3079 default_path)
3080 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3081 default_path)
3082
3083 confirm_or_exit('Move existing .gitcookies to default location?',
3084 action='move')
3085 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003086 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003087 print('Moved and reconfigured git to use .gitcookies from %s' %
3088 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003089
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003090 @staticmethod
3091 def _configure_gitcookies_path(default_path):
3092 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3093 if os.path.exists(netrc_path):
3094 print('You seem to be using outdated .netrc for git credentials: %s' %
3095 netrc_path)
3096 print('This tool will guide you through setting up recommended '
3097 '.gitcookies store for git credentials.\n'
3098 '\n'
3099 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3100 ' git config --global --unset http.cookiefile\n'
3101 ' mv %s %s.backup\n\n' % (default_path, default_path))
3102 confirm_or_exit(action='setup .gitcookies')
3103 RunGit(['config', '--global', 'http.cookiefile', default_path])
3104 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003105
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003106 def get_hosts_with_creds(self, include_netrc=False):
3107 if self._all_hosts is None:
3108 a = gerrit_util.CookiesAuthenticator()
3109 self._all_hosts = [
3110 (h, u, s)
3111 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003112 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
3113 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003114 )
3115 if h.endswith(self._GOOGLESOURCE)
3116 ]
3117
3118 if include_netrc:
3119 return self._all_hosts
3120 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3121
3122 def print_current_creds(self, include_netrc=False):
3123 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3124 if not hosts:
3125 print('No Git/Gerrit credentials found')
3126 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003127 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003128 header = [('Host', 'User', 'Which file'),
3129 ['=' * l for l in lengths]]
3130 for row in (header + hosts):
3131 print('\t'.join((('%%+%ds' % l) % s)
3132 for l, s in zip(lengths, row)))
3133
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003134 @staticmethod
3135 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003136 """Parses identity "git-<username>.domain" into <username> and domain."""
3137 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003138 # distinguishable from sub-domains. But we do know typical domains:
3139 if identity.endswith('.chromium.org'):
3140 domain = 'chromium.org'
3141 username = identity[:-len('.chromium.org')]
3142 else:
3143 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003144 if username.startswith('git-'):
3145 username = username[len('git-'):]
3146 return username, domain
3147
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003148 def _canonical_git_googlesource_host(self, host):
3149 """Normalizes Gerrit hosts (with '-review') to Git host."""
3150 assert host.endswith(self._GOOGLESOURCE)
3151 # Prefix doesn't include '.' at the end.
3152 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3153 if prefix.endswith('-review'):
3154 prefix = prefix[:-len('-review')]
3155 return prefix + '.' + self._GOOGLESOURCE
3156
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003157 def _canonical_gerrit_googlesource_host(self, host):
3158 git_host = self._canonical_git_googlesource_host(host)
3159 prefix = git_host.split('.', 1)[0]
3160 return prefix + '-review.' + self._GOOGLESOURCE
3161
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003162 def _get_counterpart_host(self, host):
3163 assert host.endswith(self._GOOGLESOURCE)
3164 git = self._canonical_git_googlesource_host(host)
3165 gerrit = self._canonical_gerrit_googlesource_host(git)
3166 return git if gerrit == host else gerrit
3167
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003168 def has_generic_host(self):
3169 """Returns whether generic .googlesource.com has been configured.
3170
3171 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3172 """
3173 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3174 if host == '.' + self._GOOGLESOURCE:
3175 return True
3176 return False
3177
3178 def _get_git_gerrit_identity_pairs(self):
3179 """Returns map from canonic host to pair of identities (Git, Gerrit).
3180
3181 One of identities might be None, meaning not configured.
3182 """
3183 host_to_identity_pairs = {}
3184 for host, identity, _ in self.get_hosts_with_creds():
3185 canonical = self._canonical_git_googlesource_host(host)
3186 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3187 idx = 0 if canonical == host else 1
3188 pair[idx] = identity
3189 return host_to_identity_pairs
3190
3191 def get_partially_configured_hosts(self):
3192 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003193 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003194 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003195 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003196
3197 def get_conflicting_hosts(self):
3198 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003199 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003200 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003201 if None not in (i1, i2) and i1 != i2)
3202
3203 def get_duplicated_hosts(self):
3204 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003205 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003206
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003207
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003208 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003209 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003210 hosts = sorted(hosts)
3211 assert hosts
3212 if extra_column_func is None:
3213 extras = [''] * len(hosts)
3214 else:
3215 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003216 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3217 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003218 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003219 lines.append(tmpl % he)
3220 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003221
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003222 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003223 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003224 yield ('.googlesource.com wildcard record detected',
3225 ['Chrome Infrastructure team recommends to list full host names '
3226 'explicitly.'],
3227 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003228
3229 dups = self.get_duplicated_hosts()
3230 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003231 yield ('The following hosts were defined twice',
3232 self._format_hosts(dups),
3233 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003234
3235 partial = self.get_partially_configured_hosts()
3236 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003237 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3238 'These hosts are missing',
3239 self._format_hosts(partial, lambda host: 'but %s defined' %
3240 self._get_counterpart_host(host)),
3241 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003242
3243 conflicting = self.get_conflicting_hosts()
3244 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003245 yield ('The following Git hosts have differing credentials from their '
3246 'Gerrit counterparts',
3247 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3248 tuple(self._get_git_gerrit_identity_pairs()[host])),
3249 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003250
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003251 def find_and_report_problems(self):
3252 """Returns True if there was at least one problem, else False."""
3253 found = False
3254 bad_hosts = set()
3255 for title, sublines, hosts in self._find_problems():
3256 if not found:
3257 found = True
3258 print('\n\n.gitcookies problem report:\n')
3259 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003260 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003261 if sublines:
3262 print()
3263 print(' %s' % '\n '.join(sublines))
3264 print()
3265
3266 if bad_hosts:
3267 assert found
3268 print(' You can manually remove corresponding lines in your %s file and '
3269 'visit the following URLs with correct account to generate '
3270 'correct credential lines:\n' %
3271 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3272 print(' %s' % '\n '.join(sorted(set(
3273 gerrit_util.CookiesAuthenticator().get_new_password_url(
3274 self._canonical_git_googlesource_host(host))
3275 for host in bad_hosts
3276 ))))
3277 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003278
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003279
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003280@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003281def CMDcreds_check(parser, args):
3282 """Checks credentials and suggests changes."""
3283 _, _ = parser.parse_args(args)
3284
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003285 # Code below checks .gitcookies. Abort if using something else.
3286 authn = gerrit_util.Authenticator.get()
3287 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003288 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003289 'This command is not designed for bot environment. It checks '
3290 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003291 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3292 if isinstance(authn, gerrit_util.GceAuthenticator):
3293 message += (
3294 '\n'
3295 'If you need to run this on GCE or a cloudtop instance, '
3296 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3297 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003298
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003299 checker = _GitCookiesChecker()
3300 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003301
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003302 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003303 checker.print_current_creds(include_netrc=True)
3304
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003305 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003306 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003307 return 0
3308 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003309
3310
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003311@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003312def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003313 """Gets or sets base-url for this branch."""
Edward Lesmes50da7702020-03-30 19:23:43 +00003314 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003315 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003316 _, args = parser.parse_args(args)
3317 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003318 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003319 return RunGit(['config', 'branch.%s.base-url' % branch],
3320 error_ok=False).strip()
3321 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003322 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003323 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3324 error_ok=False).strip()
3325
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003326
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003327def color_for_status(status):
3328 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003329 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003330 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003331 'unsent': BOLD + Fore.YELLOW,
3332 'waiting': BOLD + Fore.RED,
3333 'reply': BOLD + Fore.YELLOW,
3334 'not lgtm': BOLD + Fore.RED,
3335 'lgtm': BOLD + Fore.GREEN,
3336 'commit': BOLD + Fore.MAGENTA,
3337 'closed': BOLD + Fore.CYAN,
3338 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003339 }.get(status, Fore.WHITE)
3340
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003341
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003342def get_cl_statuses(changes, fine_grained, max_processes=None):
3343 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003344
3345 If fine_grained is true, this will fetch CL statuses from the server.
3346 Otherwise, simply indicate if there's a matching url for the given branches.
3347
3348 If max_processes is specified, it is used as the maximum number of processes
3349 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3350 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003351
3352 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003353 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003354 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003355 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003356
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003357 if not fine_grained:
3358 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003359 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003360 for cl in changes:
3361 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003362 return
3363
3364 # First, sort out authentication issues.
3365 logging.debug('ensuring credentials exist')
3366 for cl in changes:
3367 cl.EnsureAuthenticated(force=False, refresh=True)
3368
3369 def fetch(cl):
3370 try:
3371 return (cl, cl.GetStatus())
3372 except:
3373 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003374 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003375 raise
3376
3377 threads_count = len(changes)
3378 if max_processes:
3379 threads_count = max(1, min(threads_count, max_processes))
3380 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3381
Edward Lemur61bf4172020-02-24 23:22:37 +00003382 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003383 fetched_cls = set()
3384 try:
3385 it = pool.imap_unordered(fetch, changes).__iter__()
3386 while True:
3387 try:
3388 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003389 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003390 break
3391 fetched_cls.add(cl)
3392 yield cl, status
3393 finally:
3394 pool.close()
3395
3396 # Add any branches that failed to fetch.
3397 for cl in set(changes) - fetched_cls:
3398 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003399
rmistry@google.com2dd99862015-06-22 12:22:18 +00003400
Jose Lopes3863fc52020-04-07 17:00:25 +00003401def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003402 """Uploads CLs of local branches that are dependents of the current branch.
3403
3404 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003405
3406 test1 -> test2.1 -> test3.1
3407 -> test3.2
3408 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003409
3410 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3411 run on the dependent branches in this order:
3412 test2.1, test3.1, test3.2, test2.2, test3.3
3413
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003414 Note: This function does not rebase your local dependent branches. Use it
3415 when you make a change to the parent branch that will not conflict
3416 with its dependent branches, and you would like their dependencies
3417 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003418 """
3419 if git_common.is_dirty_git_tree('upload-branch-deps'):
3420 return 1
3421
3422 root_branch = cl.GetBranch()
3423 if root_branch is None:
3424 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3425 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003426 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003427 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3428 'patchset dependencies without an uploaded CL.')
3429
3430 branches = RunGit(['for-each-ref',
3431 '--format=%(refname:short) %(upstream:short)',
3432 'refs/heads'])
3433 if not branches:
3434 print('No local branches found.')
3435 return 0
3436
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003437 # Create a dictionary of all local branches to the branches that are
3438 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003439 tracked_to_dependents = collections.defaultdict(list)
3440 for b in branches.splitlines():
3441 tokens = b.split()
3442 if len(tokens) == 2:
3443 branch_name, tracked = tokens
3444 tracked_to_dependents[tracked].append(branch_name)
3445
vapiera7fbd5a2016-06-16 09:17:49 -07003446 print()
3447 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003448 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003449
rmistry@google.com2dd99862015-06-22 12:22:18 +00003450 def traverse_dependents_preorder(branch, padding=''):
3451 dependents_to_process = tracked_to_dependents.get(branch, [])
3452 padding += ' '
3453 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003454 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003455 dependents.append(dependent)
3456 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003457
rmistry@google.com2dd99862015-06-22 12:22:18 +00003458 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003459 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003460
3461 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003462 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003463 return 0
3464
Jose Lopes3863fc52020-04-07 17:00:25 +00003465 if not force:
3466 confirm_or_exit('This command will checkout all dependent branches and run '
3467 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003468
rmistry@google.com2dd99862015-06-22 12:22:18 +00003469 # Record all dependents that failed to upload.
3470 failures = {}
3471 # Go through all dependents, checkout the branch and upload.
3472 try:
3473 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003474 print()
3475 print('--------------------------------------')
3476 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003477 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003478 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003479 try:
3480 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003481 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003482 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003483 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003484 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003485 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003486 finally:
3487 # Swap back to the original root branch.
3488 RunGit(['checkout', '-q', root_branch])
3489
vapiera7fbd5a2016-06-16 09:17:49 -07003490 print()
3491 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003492 for dependent_branch in dependents:
3493 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003494 print(' %s : %s' % (dependent_branch, upload_status))
3495 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003496
3497 return 0
3498
3499
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003500def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003501 """Given a proposed tag name, returns a tag name that is guaranteed to be
3502 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3503 or 'foo-3', and so on."""
3504
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003505 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003506 for suffix_num in itertools.count(1):
3507 if suffix_num == 1:
3508 to_check = proposed_tag
3509 else:
3510 to_check = '%s-%d' % (proposed_tag, suffix_num)
3511
3512 if to_check not in existing_tags:
3513 return to_check
3514
3515
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003516@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003517def CMDarchive(parser, args):
3518 """Archives and deletes branches associated with closed changelists."""
3519 parser.add_option(
3520 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003521 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003522 parser.add_option(
3523 '-f', '--force', action='store_true',
3524 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003525 parser.add_option(
3526 '-d', '--dry-run', action='store_true',
3527 help='Skip the branch tagging and removal steps.')
3528 parser.add_option(
3529 '-t', '--notags', action='store_true',
3530 help='Do not tag archived branches. '
3531 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003532 parser.add_option(
3533 '-p',
3534 '--pattern',
3535 default='git-cl-archived-{issue}-{branch}',
3536 help='Format string for archive tags. '
3537 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003538
kmarshall3bff56b2016-06-06 18:31:47 -07003539 options, args = parser.parse_args(args)
3540 if args:
3541 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003542
3543 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3544 if not branches:
3545 return 0
3546
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003547 tags = RunGit(['for-each-ref', '--format=%(refname)',
3548 'refs/tags']).splitlines() or []
3549 tags = [t.split('/')[-1] for t in tags]
3550
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003552 changes = [Changelist(branchref=b)
3553 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003554 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3555 statuses = get_cl_statuses(changes,
3556 fine_grained=True,
3557 max_processes=options.maxjobs)
3558 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003559 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3560 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003561 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003562 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003563 proposal.sort()
3564
3565 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003566 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003567 return 0
3568
Edward Lemur85153282020-02-14 22:06:29 +00003569 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003570
vapiera7fbd5a2016-06-16 09:17:49 -07003571 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003572 if options.notags:
3573 for next_item in proposal:
3574 print(' ' + next_item[0])
3575 else:
3576 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3577 for next_item in proposal:
3578 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003579
kmarshall9249e012016-08-23 12:02:16 -07003580 # Quit now on precondition failure or if instructed by the user, either
3581 # via an interactive prompt or by command line flags.
3582 if options.dry_run:
3583 print('\nNo changes were made (dry run).\n')
3584 return 0
3585 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003586 print('You are currently on a branch \'%s\' which is associated with a '
3587 'closed codereview issue, so archive cannot proceed. Please '
3588 'checkout another branch and run this command again.' %
3589 current_branch)
3590 return 1
kmarshall9249e012016-08-23 12:02:16 -07003591 elif not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00003592 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07003593 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003595 return 1
3596
3597 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003598 if not options.notags:
3599 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003600
3601 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3602 # Clean up the tag if we failed to delete the branch.
3603 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003604
vapiera7fbd5a2016-06-16 09:17:49 -07003605 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003606
3607 return 0
3608
3609
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003610@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003611def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003612 """Show status of changelists.
3613
3614 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003615 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003616 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003617 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003618 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003619 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003620 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003621 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003622
3623 Also see 'git cl comments'.
3624 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003625 parser.add_option(
3626 '--no-branch-color',
3627 action='store_true',
3628 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003630 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003631 parser.add_option('-f', '--fast', action='store_true',
3632 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003633 parser.add_option(
3634 '-j', '--maxjobs', action='store', type=int,
3635 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003636 parser.add_option(
3637 '-i', '--issue', type=int,
3638 help='Operate on this issue instead of the current branch\'s implicit '
3639 'issue. Requires --field to be set.')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003640 parser.add_option('-d',
3641 '--date-order',
3642 action='store_true',
3643 help='Order branches by committer date.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003644 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003645 if args:
3646 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647
iannuccie53c9352016-08-17 14:40:40 -07003648 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003649 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003651 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003652 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003653 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003654 if cl.GetIssue():
3655 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 elif options.field == 'id':
3657 issueid = cl.GetIssue()
3658 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003659 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003661 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003663 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003664 elif options.field == 'status':
3665 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003666 elif options.field == 'url':
3667 url = cl.GetIssueURL()
3668 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003669 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003670 return 0
3671
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003672 branches = RunGit([
3673 'for-each-ref', '--format=%(refname) %(committerdate:unix)', 'refs/heads'
3674 ])
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003675 if not branches:
3676 print('No local branch found.')
3677 return 0
3678
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003679 changes = [
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003680 Changelist(branchref=b, commit_date=ct)
3681 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
3682 ]
vapiera7fbd5a2016-06-16 09:17:49 -07003683 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003684 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003685 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003686 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003687
Edward Lemur85153282020-02-14 22:06:29 +00003688 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00003689
3690 def FormatBranchName(branch, colorize=False):
3691 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3692 an asterisk when it is the current branch."""
3693
3694 asterisk = ""
3695 color = Fore.RESET
3696 if branch == current_branch:
3697 asterisk = "* "
3698 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00003699 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00003700
3701 if colorize:
3702 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003703 return asterisk + branch_name
3704
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003705 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003706
3707 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00003708
3709 if options.date_order or settings.IsStatusCommitOrderByDate():
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003710 sorted_changes = sorted(changes,
3711 key=lambda c: c.GetCommitDate(),
3712 reverse=True)
3713 else:
3714 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
3715 for cl in sorted_changes:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003716 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003717 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003718 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003719 branch_statuses[c.GetBranch()] = status
3720 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00003721 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003722 if url and (not status or status == 'error'):
3723 # The issue probably doesn't exist anymore.
3724 url += ' (broken)'
3725
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003726 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003727 # Turn off bold as well as colors.
3728 END = '\033[0m'
3729 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003730 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003731 color = ''
3732 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003733 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003734
Alan Cuttera3be9a52019-03-04 18:50:33 +00003735 branch_display = FormatBranchName(branch)
3736 padding = ' ' * (alignment - len(branch_display))
3737 if not options.no_branch_color:
3738 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003739
Alan Cuttera3be9a52019-03-04 18:50:33 +00003740 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3741 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003742
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003744 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003745 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003746 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003747 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003748 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003749 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003750 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003751 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003752 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003753 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003754 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003755 return 0
3756
3757
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003758def colorize_CMDstatus_doc():
3759 """To be called once in main() to add colors to git cl status help."""
3760 colors = [i for i in dir(Fore) if i[0].isupper()]
3761
3762 def colorize_line(line):
3763 for color in colors:
3764 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003765 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003766 indent = len(line) - len(line.lstrip(' ')) + 1
3767 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3768 return line
3769
3770 lines = CMDstatus.__doc__.splitlines()
3771 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3772
3773
phajdan.jre328cf92016-08-22 04:12:17 -07003774def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003775 if path == '-':
3776 json.dump(contents, sys.stdout)
3777 else:
3778 with open(path, 'w') as f:
3779 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003780
3781
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003782@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003783@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003785 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786
3787 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003788 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003789 parser.add_option('-r', '--reverse', action='store_true',
3790 help='Lookup the branch(es) for the specified issues. If '
3791 'no issues are specified, all branches with mapped '
3792 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003793 parser.add_option('--json',
3794 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003795 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796
dnj@chromium.org406c4402015-03-03 17:22:28 +00003797 if options.reverse:
3798 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003799 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003800 # Reverse issue lookup.
3801 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003802
3803 git_config = {}
3804 for config in RunGit(['config', '--get-regexp',
3805 r'branch\..*issue']).splitlines():
3806 name, _space, val = config.partition(' ')
3807 git_config[name] = val
3808
dnj@chromium.org406c4402015-03-03 17:22:28 +00003809 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00003810 issue = git_config.get(
3811 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00003812 if issue:
3813 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003814 if not args:
Carlos Caballero81923d62020-07-06 18:22:27 +00003815 args = sorted(issue_branch_map.keys())
phajdan.jre328cf92016-08-22 04:12:17 -07003816 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003817 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003818 try:
3819 issue_num = int(issue)
3820 except ValueError:
3821 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003822 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003823 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003824 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003825 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003826 if options.json:
3827 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003828 return 0
3829
3830 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003831 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003832 if not issue.valid:
3833 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3834 'or no argument to list it.\n'
3835 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003836 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003837 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003838 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003839 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003840 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
3841 if options.json:
3842 write_json(options.json, {
Nodir Turakulov27379632021-03-17 18:53:29 +00003843 'gerrit_host': cl.GetGerritHost(),
3844 'gerrit_project': cl.GetGerritProject(),
Aaron Gable78753da2017-06-15 10:35:49 -07003845 'issue_url': cl.GetIssueURL(),
Nodir Turakulov27379632021-03-17 18:53:29 +00003846 'issue': cl.GetIssue(),
Aaron Gable78753da2017-06-15 10:35:49 -07003847 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003848 return 0
3849
3850
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003851@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003852def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003853 """Shows or posts review comments for any changelist."""
3854 parser.add_option('-a', '--add-comment', dest='comment',
3855 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003856 parser.add_option('-p', '--publish', action='store_true',
3857 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01003858 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00003859 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003860 parser.add_option('-m', '--machine-readable', dest='readable',
3861 action='store_false', default=True,
3862 help='output comments in a format compatible with '
3863 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00003864 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07003865 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003866 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003867
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003868 issue = None
3869 if options.issue:
3870 try:
3871 issue = int(options.issue)
3872 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003873 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003874
Edward Lemur934836a2019-09-09 20:16:54 +00003875 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003876
3877 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003878 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003879 return 0
3880
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003881 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
3882 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003883 for comment in summary:
3884 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003885 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003886 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003887 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003888 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003889 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00003890 elif comment.autogenerated:
3891 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003892 else:
3893 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003894 print('\n%s%s %s%s\n%s' % (
3895 color,
3896 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
3897 comment.sender,
3898 Fore.RESET,
3899 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
3900
smut@google.comc85ac942015-09-15 16:34:43 +00003901 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003902 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00003903 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003904 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
3905 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00003906 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003907 return 0
3908
3909
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003910@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003911@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003912def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003913 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003914 parser.add_option('-d', '--display', action='store_true',
3915 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003916 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003917 help='New description to set for this issue (- for stdin, '
3918 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003919 parser.add_option('-f', '--force', action='store_true',
3920 help='Delete any unpublished Gerrit edits for this issue '
3921 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003922
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003923 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003924
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003925 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003926 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003927 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003928 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003929 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003930
Edward Lemur934836a2019-09-09 20:16:54 +00003931 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003932 if target_issue_arg:
3933 kwargs['issue'] = target_issue_arg.issue
3934 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003935
3936 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003937 if not cl.GetIssue():
3938 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003939
Edward Lemur678a6842019-10-03 22:25:05 +00003940 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00003941 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003942
Edward Lemur6c6827c2020-02-06 21:15:18 +00003943 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003944
smut@google.com34fb6b12015-07-13 20:03:26 +00003945 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003946 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003947 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003948
3949 if options.new_description:
3950 text = options.new_description
3951 if text == '-':
3952 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003953 elif text == '+':
3954 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00003955 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003956
3957 description.set_description(text)
3958 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003959 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00003960 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003961 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003962 return 0
3963
3964
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003965@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003966def CMDlint(parser, args):
3967 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003968 parser.add_option('--filter', action='append', metavar='-x,+y',
3969 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003970 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003971
3972 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003973 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00003974 try:
3975 import cpplint
3976 import cpplint_chromium
3977 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003978 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003979 return 1
3980
3981 # Change the current working directory before calling lint so that it
3982 # shows the correct base.
3983 previous_cwd = os.getcwd()
3984 os.chdir(settings.GetRoot())
3985 try:
Edward Lemur934836a2019-09-09 20:16:54 +00003986 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00003987 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003988 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003990 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003991
Lei Zhangb8c62cf2020-07-15 20:09:37 +00003992 # Process cpplint arguments, if any.
3993 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
3994 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003995 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003996
Lei Zhang379d1ad2020-07-15 19:40:06 +00003997 include_regex = re.compile(settings.GetLintRegex())
3998 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00003999 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4000 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00004001 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004002 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00004003 continue
4004
4005 if ignore_regex.match(filename):
4006 print('Ignoring file %s' % filename)
4007 continue
4008
4009 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4010 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004011 finally:
4012 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004013 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004014 if cpplint._cpplint_state.error_count != 0:
4015 return 1
4016 return 0
4017
4018
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004019@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004020def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004021 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004022 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004023 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004024 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004025 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004026 parser.add_option('--all', action='store_true',
4027 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004028 parser.add_option('--parallel', action='store_true',
4029 help='Run all tests specified by input_api.RunTests in all '
4030 'PRESUBMIT files in parallel.')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004031 parser.add_option('--resultdb', action='store_true',
4032 help='Run presubmit checks in the ResultSink environment '
4033 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004034 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004035 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004036
sbc@chromium.org71437c02015-04-09 19:29:40 +00004037 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004038 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004039 return 1
4040
Edward Lemur934836a2019-09-09 20:16:54 +00004041 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042 if args:
4043 base_branch = args[0]
4044 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004045 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004046 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004047
Gregory Nisbet29d5cf82020-02-27 08:16:58 +00004048 if cl.GetIssue():
4049 description = cl.FetchDescription()
Aaron Gable8076c282017-11-29 14:39:41 -08004050 else:
Edward Lemura12175c2020-03-09 16:58:26 +00004051 description = _create_description_from_log([base_branch])
Aaron Gable8076c282017-11-29 14:39:41 -08004052
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004053 cl.RunHook(
4054 committing=not options.upload,
4055 may_prompt=False,
4056 verbose=options.verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00004057 parallel=options.parallel,
4058 upstream=base_branch,
4059 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004060 all_files=options.all,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004061 resultdb=options.resultdb,
4062 realm=options.realm)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004063 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064
4065
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004066def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004067 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004068
4069 Works the same way as
4070 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4071 but can be called on demand on all platforms.
4072
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004073 The basic idea is to generate git hash of a state of the tree, original
4074 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004075 """
4076 lines = []
4077 tree_hash = RunGitSilent(['write-tree'])
4078 lines.append('tree %s' % tree_hash.strip())
4079 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4080 if code == 0:
4081 lines.append('parent %s' % parent.strip())
4082 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4083 lines.append('author %s' % author.strip())
4084 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4085 lines.append('committer %s' % committer.strip())
4086 lines.append('')
4087 # Note: Gerrit's commit-hook actually cleans message of some lines and
4088 # whitespace. This code is not doing this, but it clearly won't decrease
4089 # entropy.
4090 lines.append(message)
4091 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004092 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004093 return 'I%s' % change_hash.strip()
4094
4095
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004096def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004097 """Computes the remote branch ref to use for the CL.
4098
4099 Args:
4100 remote (str): The git remote for the CL.
4101 remote_branch (str): The git remote branch for the CL.
4102 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004103 """
4104 if not (remote and remote_branch):
4105 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004106
wittman@chromium.org455dc922015-01-26 20:15:50 +00004107 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004108 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004109 # refs, which are then translated into the remote full symbolic refs
4110 # below.
4111 if '/' not in target_branch:
4112 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4113 else:
4114 prefix_replacements = (
4115 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4116 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4117 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4118 )
4119 match = None
4120 for regex, replacement in prefix_replacements:
4121 match = re.search(regex, target_branch)
4122 if match:
4123 remote_branch = target_branch.replace(match.group(0), replacement)
4124 break
4125 if not match:
4126 # This is a branch path but not one we recognize; use as-is.
4127 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004128 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4129 # Handle the refs that need to land in different refs.
4130 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004131
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004132 # Migration to new default branch, only if available on remote.
4133 allow_push_on_master = bool(os.environ.get("ALLOW_PUSH_TO_MASTER", None))
4134 if remote_branch == DEFAULT_OLD_BRANCH and not allow_push_on_master:
4135 if RunGit(['show-branch', DEFAULT_NEW_BRANCH], error_ok=True,
4136 stderr=subprocess2.PIPE):
4137 # TODO(crbug.com/ID): Print location to local git migration script.
4138 print("WARNING: Using new branch name %s instead of %s" % (
4139 DEFAULT_NEW_BRANCH, DEFAULT_OLD_BRANCH))
4140 remote_branch = DEFAULT_NEW_BRANCH
4141
wittman@chromium.org455dc922015-01-26 20:15:50 +00004142 # Create the true path to the remote branch.
4143 # Does the following translation:
4144 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004145 # * refs/remotes/origin/main -> refs/heads/main
wittman@chromium.org455dc922015-01-26 20:15:50 +00004146 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4147 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4148 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4149 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4150 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4151 'refs/heads/')
4152 elif remote_branch.startswith('refs/remotes/branch-heads'):
4153 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004154
wittman@chromium.org455dc922015-01-26 20:15:50 +00004155 return remote_branch
4156
4157
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004158def cleanup_list(l):
4159 """Fixes a list so that comma separated items are put as individual items.
4160
4161 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4162 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4163 """
4164 items = sum((i.split(',') for i in l), [])
4165 stripped_items = (i.strip() for i in items)
4166 return sorted(filter(None, stripped_items))
4167
4168
Aaron Gable4db38df2017-11-03 14:59:07 -07004169@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004170@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004171def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004172 """Uploads the current changelist to codereview.
4173
4174 Can skip dependency patchset uploads for a branch by running:
4175 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004176 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004177 git config --unset branch.branch_name.skip-deps-uploads
4178 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004179
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004180 If the name of the checked out branch starts with "bug-" or "fix-" followed
4181 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004182 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004183
4184 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004185 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004186 [git-cl] add support for hashtags
4187 Foo bar: implement foo
4188 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004189 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004190 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4191 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004192 parser.add_option('--bypass-watchlists', action='store_true',
4193 dest='bypass_watchlists',
4194 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004195 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004196 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004197 parser.add_option('--message', '-m', dest='message',
4198 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004199 parser.add_option('-b', '--bug',
4200 help='pre-populate the bug number(s) for this issue. '
4201 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004202 parser.add_option('--message-file', dest='message_file',
4203 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004204 parser.add_option('--title', '-t', dest='title',
4205 help='title for patchset')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004206 parser.add_option('-T', '--skip-title', action='store_true',
4207 dest='skip_title',
4208 help='Use the most recent commit message as the title of '
4209 'the patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004210 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004211 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004212 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004213 parser.add_option('--tbrs',
4214 action='append', default=[],
4215 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004216 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004217 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004218 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004219 parser.add_option('--hashtag', dest='hashtags',
4220 action='append', default=[],
4221 help=('Gerrit hashtag for new CL; '
4222 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004223 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004224 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004225 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004226 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004227 metavar='TARGET',
4228 help='Apply CL to remote ref TARGET. ' +
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004229 'Default: remote branch head, or main')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004230 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004231 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004232 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004233 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004234 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004235 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004236 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4237 const='TBR', help='add a set of OWNERS to TBR')
4238 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4239 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004240 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004241 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004242 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004243 'implies --send-mail')
4244 parser.add_option('-d', '--cq-dry-run',
4245 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004246 help='Send the patchset to do a CQ dry run right after '
4247 'upload.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004248 parser.add_option(
4249 '-q',
4250 '--cq-quick-run',
4251 action='store_true',
4252 default=False,
4253 help='Send the patchset to do a CQ quick run right after '
4254 'upload (https://source.chromium.org/chromium/chromium/src/+/main:do'
4255 'cs/cq_quick_run.md) (chromium only).')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00004256 parser.add_option('--set-bot-commit', action='store_true',
4257 help=optparse.SUPPRESS_HELP)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004258 parser.add_option('--preserve-tryjobs', action='store_true',
4259 help='instruct the CQ to let tryjobs running even after '
4260 'new patchsets are uploaded instead of canceling '
4261 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004262 parser.add_option('--dependencies', action='store_true',
4263 help='Uploads CLs of all the local branches that depend on '
4264 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004265 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4266 help='Sends your change to the CQ after an approval. Only '
4267 'works on repos that have the Auto-Submit label '
4268 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004269 parser.add_option('--parallel', action='store_true',
4270 help='Run all tests specified by input_api.RunTests in all '
4271 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004272 parser.add_option('--no-autocc', action='store_true',
4273 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004274 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004275 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004276 parser.add_option('-R', '--retry-failed', action='store_true',
4277 help='Retry failed tryjobs from old patchset immediately '
4278 'after uploading new patchset. Cannot be used with '
4279 '--use-commit-queue or --cq-dry-run.')
4280 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4281 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004282 parser.add_option('--fixed', '-x',
4283 help='List of bugs that will be commented on and marked '
4284 'fixed (pre-populates "Fixed:" tag). Same format as '
4285 '-b option / "Bug:" tag. If fixing several issues, '
4286 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004287 parser.add_option('--edit-description', action='store_true', default=False,
4288 help='Modify description before upload. Cannot be used '
4289 'with --force. It is a noop when --no-squash is set '
4290 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004291 parser.add_option('--git-completion-helper', action="store_true",
4292 help=optparse.SUPPRESS_HELP)
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004293 parser.add_option('--resultdb', action='store_true',
4294 help='Run presubmit checks in the ResultSink environment '
4295 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004296 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00004297 parser.add_option('-o',
4298 '--push-options',
4299 action='append',
4300 default=[],
4301 help='Transmit the given string to the server when '
4302 'performing git push (pass-through). See git-push '
4303 'documentation for more details.')
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00004304 parser.add_option('--no-add-changeid',
4305 action='store_true',
4306 dest='no_add_changeid',
4307 help='Do not add change-ids to messages.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004308
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004310 (options, args) = parser.parse_args(args)
4311
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004312 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004313 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4314 if opt.help != optparse.SUPPRESS_HELP))
4315 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004316
sbc@chromium.org71437c02015-04-09 19:29:40 +00004317 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004318 return 1
4319
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004320 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004321 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004322 options.cc = cleanup_list(options.cc)
4323
Josipe827b0f2020-01-30 00:07:20 +00004324 if options.edit_description and options.force:
4325 parser.error('Only one of --force and --edit-description allowed')
4326
tandriib80458a2016-06-23 12:20:07 -07004327 if options.message_file:
4328 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004329 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004330 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07004331
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004332 if ([options.cq_dry_run,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004333 options.cq_quick_run,
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004334 options.use_commit_queue,
4335 options.retry_failed].count(True) > 1):
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004336 parser.error('Only one of --use-commit-queue, --cq-dry-run, --cq-quick-run '
4337 'or --retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004338
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004339 if options.skip_title and options.title:
4340 parser.error('Only one of --title and --skip-title allowed.')
4341
Aaron Gableedbc4132017-09-11 13:22:28 -07004342 if options.use_commit_queue:
4343 options.send_mail = True
4344
Edward Lesmes0dd54822020-03-26 18:24:25 +00004345 if options.squash is None:
4346 # Load default for user, repo, squash=true, in this order.
4347 options.squash = settings.GetSquashGerritUploads()
4348
Josip Sokcevic1cabb172021-04-08 19:15:35 +00004349 cl = Changelist(branchref=options.target_branch)
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004350 # Warm change details cache now to avoid RPCs later, reducing latency for
4351 # developers.
4352 if cl.GetIssue():
4353 cl._GetChangeDetail(
4354 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4355
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004356 if options.retry_failed and not cl.GetIssue():
4357 print('No previous patchsets, so --retry-failed has no effect.')
4358 options.retry_failed = False
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004359
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004360 # cl.GetMostRecentPatchset uses cached information, and can return the last
4361 # patchset before upload. Calling it here makes it clear that it's the
4362 # last patchset before upload. Note that GetMostRecentPatchset will fail
4363 # if no CL has been uploaded yet.
4364 if options.retry_failed:
4365 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004366
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004367 ret = cl.CMDUpload(options, args, orig_args)
4368
4369 if options.retry_failed:
4370 if ret != 0:
4371 print('Upload failed, so --retry-failed has no effect.')
4372 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004373 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004374 cl, options.buildbucket_host, latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004375 jobs = _filter_failed_for_retry(builds)
4376 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004377 print('No failed tryjobs, so --retry-failed has no effect.')
4378 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004379 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004380
4381 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004382
4383
Francois Dorayd42c6812017-05-30 15:10:20 -04004384@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004385@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004386def CMDsplit(parser, args):
4387 """Splits a branch into smaller branches and uploads CLs.
4388
4389 Creates a branch and uploads a CL for each group of files modified in the
4390 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00004391 comment, the string '$directory', is replaced with the directory containing
4392 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04004393 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004394 parser.add_option('-d', '--description', dest='description_file',
4395 help='A text file containing a CL description in which '
4396 '$directory will be replaced by each CL\'s directory.')
4397 parser.add_option('-c', '--comment', dest='comment_file',
4398 help='A text file containing a CL comment.')
4399 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004400 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004401 help='List the files and reviewers for each CL that would '
4402 'be created, but don\'t create branches or CLs.')
4403 parser.add_option('--cq-dry-run', action='store_true',
4404 help='If set, will do a cq dry run for each uploaded CL. '
4405 'Please be careful when doing this; more than ~10 CLs '
4406 'has the potential to overload our build '
4407 'infrastructure. Try to upload these not during high '
4408 'load times (usually 11-3 Mountain View time). Email '
4409 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004410 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4411 default=True,
4412 help='Sends your change to the CQ after an approval. Only '
4413 'works on repos that have the Auto-Submit label '
4414 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004415 options, _ = parser.parse_args(args)
4416
4417 if not options.description_file:
4418 parser.error('No --description flag specified.')
4419
4420 def WrappedCMDupload(args):
4421 return CMDupload(OptionParser(), args)
4422
Edward Lemur2c62b332020-03-12 22:12:33 +00004423 return split_cl.SplitCl(
4424 options.description_file, options.comment_file, Changelist,
4425 WrappedCMDupload, options.dry_run, options.cq_dry_run,
4426 options.enable_auto_submit, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04004427
4428
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004429@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004430@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004432 """DEPRECATED: Used to commit the current changelist via git-svn."""
4433 message = ('git-cl no longer supports committing to SVN repositories via '
4434 'git-svn. You probably want to use `git cl land` instead.')
4435 print(message)
4436 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437
4438
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004439@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004440@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004441def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004442 """Commits the current changelist via git.
4443
4444 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4445 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004446 """
4447 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4448 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004449 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004450 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004451 parser.add_option('--parallel', action='store_true',
4452 help='Run all tests specified by input_api.RunTests in all '
4453 'PRESUBMIT files in parallel.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004454 parser.add_option('--resultdb', action='store_true',
4455 help='Run presubmit checks in the ResultSink environment '
4456 'and send results to the ResultDB database.')
4457 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004458 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004459
Edward Lemur934836a2019-09-09 20:16:54 +00004460 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004461
Robert Iannucci2e73d432018-03-14 01:10:47 -07004462 if not cl.GetIssue():
4463 DieWithError('You must upload the change first to Gerrit.\n'
4464 ' If you would rather have `git cl land` upload '
4465 'automatically for you, see http://crbug.com/642759')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004466 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
4467 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004468
4469
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004470@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004471@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004473 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004474 parser.add_option('-b', dest='newbranch',
4475 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004476 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004477 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004478 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004479 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004480
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004481 group = optparse.OptionGroup(
4482 parser,
4483 'Options for continuing work on the current issue uploaded from a '
4484 'different clone (e.g. different machine). Must be used independently '
4485 'from the other options. No issue number should be specified, and the '
4486 'branch must have an issue number associated with it')
4487 group.add_option('--reapply', action='store_true', dest='reapply',
4488 help='Reset the branch and reapply the issue.\n'
4489 'CAUTION: This will undo any local changes in this '
4490 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004491
4492 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004493 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004494 parser.add_option_group(group)
4495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004497
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004498 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004499 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004500 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004501 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004502 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004503
Edward Lemur934836a2019-09-09 20:16:54 +00004504 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004505 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004506 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004507
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004508 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004509 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004510 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004511
4512 RunGit(['reset', '--hard', upstream])
4513 if options.pull:
4514 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004515
Edward Lemur678a6842019-10-03 22:25:05 +00004516 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00004517 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False,
4518 False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004519
4520 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004521 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004522
Edward Lemurf38bc172019-09-03 21:02:13 +00004523 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004524 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004525 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004526
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004527 # We don't want uncommitted changes mixed up with the patch.
4528 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004529 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004531 if options.newbranch:
4532 if options.force:
4533 RunGit(['branch', '-D', options.newbranch],
4534 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00004535 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004536
Edward Lemur678a6842019-10-03 22:25:05 +00004537 cl = Changelist(
4538 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004539
Edward Lemur678a6842019-10-03 22:25:05 +00004540 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004541 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004542
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00004543 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
4544 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545
4546
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004547def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004548 """Fetches the tree status and returns either 'open', 'closed',
4549 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004550 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004551 if url:
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00004552 status = str(urllib.request.urlopen(url).read().lower())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004553 if status.find('closed') != -1 or status == '0':
4554 return 'closed'
4555 elif status.find('open') != -1 or status == '1':
4556 return 'open'
4557 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004558 return 'unset'
4559
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004560
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561def GetTreeStatusReason():
4562 """Fetches the tree status from a json url and returns the message
4563 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004564 url = settings.GetTreeStatusUrl()
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00004565 json_url = urllib.parse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004566 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567 status = json.loads(connection.read())
4568 connection.close()
4569 return status['message']
4570
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004571
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004572@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004573def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004574 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004575 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004576 status = GetTreeStatus()
4577 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004578 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004579 return 2
4580
vapiera7fbd5a2016-06-16 09:17:49 -07004581 print('The tree is %s' % status)
4582 print()
4583 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004584 if status != 'open':
4585 return 1
4586 return 0
4587
4588
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004589@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004590def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004591 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4592 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004593 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004594 '-b', '--bot', action='append',
4595 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4596 'times to specify multiple builders. ex: '
4597 '"-b win_rel -b win_layout". See '
4598 'the try server waterfall for the builders name and the tests '
4599 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004600 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004601 '-B', '--bucket', default='',
4602 help=('Buildbucket bucket to send the try requests.'))
4603 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004604 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004605 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004606 'be determined by the try recipe that builder runs, which usually '
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004607 'defaults to HEAD of origin/master or origin/main')
maruel@chromium.org15192402012-09-06 12:38:29 +00004608 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004609 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004610 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004611 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004612 group.add_option(
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004613 '-q',
4614 '--quick-run',
4615 action='store_true',
4616 default=False,
4617 help='trigger in quick run mode '
4618 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_q'
4619 'uick_run.md) (chromium only).')
4620 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004621 '--category', default='git_cl_try', help='Specify custom build category.')
4622 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004623 '--project',
4624 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004625 'in recipe to determine to which repository or directory to '
4626 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004627 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004628 '-p', '--property', dest='properties', action='append', default=[],
4629 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004630 'key2=value2 etc. The value will be treated as '
4631 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004632 'NOTE: using this may make your tryjob not usable for CQ, '
4633 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004634 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004635 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4636 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004637 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004638 parser.add_option(
4639 '-R', '--retry-failed', action='store_true', default=False,
4640 help='Retry failed jobs from the latest set of tryjobs. '
4641 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004642 parser.add_option(
4643 '-i', '--issue', type=int,
4644 help='Operate on this issue instead of the current branch\'s implicit '
4645 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004646 options, args = parser.parse_args(args)
4647
machenbach@chromium.org45453142015-09-15 08:45:22 +00004648 # Make sure that all properties are prop=value pairs.
4649 bad_params = [x for x in options.properties if '=' not in x]
4650 if bad_params:
4651 parser.error('Got properties with missing "=": %s' % bad_params)
4652
maruel@chromium.org15192402012-09-06 12:38:29 +00004653 if args:
4654 parser.error('Unknown arguments: %s' % args)
4655
Edward Lemur934836a2019-09-09 20:16:54 +00004656 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004657 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004658 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004659
Edward Lemurf38bc172019-09-03 21:02:13 +00004660 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004661 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004662
tandriie113dfd2016-10-11 10:20:12 -07004663 error_message = cl.CannotTriggerTryJobReason()
4664 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004665 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004666
Edward Lemur45768512020-03-02 19:03:14 +00004667 if options.bot:
4668 if options.retry_failed:
4669 parser.error('--bot is not compatible with --retry-failed.')
4670 if not options.bucket:
4671 parser.error('A bucket (e.g. "chromium/try") is required.')
4672
4673 triggered = [b for b in options.bot if 'triggered' in b]
4674 if triggered:
4675 parser.error(
4676 'Cannot schedule builds on triggered bots: %s.\n'
4677 'This type of bot requires an initial job from a parent (usually a '
4678 'builder). Schedule a job on the parent instead.\n' % triggered)
4679
4680 if options.bucket.startswith('.master'):
4681 parser.error('Buildbot masters are not supported.')
4682
4683 project, bucket = _parse_bucket(options.bucket)
4684 if project is None or bucket is None:
4685 parser.error('Invalid bucket: %s.' % options.bucket)
4686 jobs = sorted((project, bucket, bot) for bot in options.bot)
4687 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004688 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004689 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004690 if options.verbose:
4691 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00004692 jobs = _filter_failed_for_retry(builds)
4693 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004694 print('There are no failed jobs in the latest set of jobs '
4695 '(patchset #%d), doing nothing.' % patchset)
4696 return 0
Edward Lemur45768512020-03-02 19:03:14 +00004697 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004698 if num_builders > 10:
4699 confirm_or_exit('There are %d builders with failed builds.'
4700 % num_builders, action='continue')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004701 elif options.quick_run:
4702 print('Scheduling CQ quick run on: %s' % cl.GetIssueURL())
4703 return cl.SetCQState(_CQState.QUICK_RUN)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004704 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07004705 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004706 print('git cl try with no bots now defaults to CQ dry run.')
4707 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4708 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004709
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004710 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004711 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004712 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004713 except BuildbucketResponseException as ex:
4714 print('ERROR: %s' % ex)
4715 return 1
4716 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004717
4718
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004719@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004720def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004721 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004722 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004723 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004724 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004725 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004726 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004727 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004728 '--color', action='store_true', default=setup_color.IS_TTY,
4729 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004730 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004731 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4732 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004733 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004734 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004735 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004736 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00004737 parser.add_option(
4738 '-i', '--issue', type=int,
4739 help='Operate on this issue instead of the current branch\'s implicit '
4740 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004741 options, args = parser.parse_args(args)
4742 if args:
4743 parser.error('Unrecognized args: %s' % ' '.join(args))
4744
Edward Lemur934836a2019-09-09 20:16:54 +00004745 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004746 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004747 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004748
tandrii221ab252016-10-06 08:12:04 -07004749 patchset = options.patchset
4750 if not patchset:
Gavin Make61ccc52020-11-13 00:12:57 +00004751 patchset = cl.GetMostRecentDryRunPatchset()
tandrii221ab252016-10-06 08:12:04 -07004752 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004753 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004754 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004755 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004756 cl.GetIssue())
4757
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004758 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004759 jobs = _fetch_tryjobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004760 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004761 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004762 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004763 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004764 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004765 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004766 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004767 return 0
4768
4769
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004770@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004771@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004773 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004774 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004775 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004776 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004777
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004778 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004779 if args:
4780 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004781 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004782 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004783 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004784 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004785
4786 # Clear configured merge-base, if there is one.
4787 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004788 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004789 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004790 return 0
4791
4792
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004793@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004794def CMDweb(parser, args):
4795 """Opens the current CL in the web browser."""
4796 _, args = parser.parse_args(args)
4797 if args:
4798 parser.error('Unrecognized args: %s' % ' '.join(args))
4799
4800 issue_url = Changelist().GetIssueURL()
4801 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004802 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004803 return 1
4804
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004805 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004806 # allows us to hide the "Created new window in existing browser session."
4807 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004808 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004809 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004810 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004811 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004812 os.open(os.devnull, os.O_RDWR)
4813 try:
4814 webbrowser.open(issue_url)
4815 finally:
4816 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004817 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004818 return 0
4819
4820
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004821@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004822def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004823 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004824 parser.add_option('-d', '--dry-run', action='store_true',
4825 help='trigger in dry run mode')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004826 parser.add_option(
4827 '-q',
4828 '--quick-run',
4829 action='store_true',
4830 help='trigger in quick run mode '
4831 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_qu'
4832 'ick_run.md) (chromium only).')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004833 parser.add_option('-c', '--clear', action='store_true',
4834 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00004835 parser.add_option(
4836 '-i', '--issue', type=int,
4837 help='Operate on this issue instead of the current branch\'s implicit '
4838 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004839 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004840 if args:
4841 parser.error('Unrecognized args: %s' % ' '.join(args))
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004842 if [options.dry_run, options.quick_run, options.clear].count(True) > 1:
4843 parser.error('Only one of --dry-run, --quick-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004844
Edward Lemur934836a2019-09-09 20:16:54 +00004845 cl = Changelist(issue=options.issue)
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004846 if not cl.GetIssue():
4847 parser.error('Must upload the issue first.')
4848
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004849 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004850 state = _CQState.NONE
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004851 elif options.quick_run:
4852 state = _CQState.QUICK_RUN
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004853 elif options.dry_run:
4854 state = _CQState.DRY_RUN
4855 else:
4856 state = _CQState.COMMIT
tandrii9de9ec62016-07-13 03:01:59 -07004857 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004858 return 0
4859
4860
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004861@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004862def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004863 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00004864 parser.add_option(
4865 '-i', '--issue', type=int,
4866 help='Operate on this issue instead of the current branch\'s implicit '
4867 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004868 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004869 if args:
4870 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004871 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004872 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004873 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004874 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004875 cl.CloseIssue()
4876 return 0
4877
4878
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004879@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004880def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004881 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004882 parser.add_option(
4883 '--stat',
4884 action='store_true',
4885 dest='stat',
4886 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004887 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004888 if args:
4889 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004890
Edward Lemur934836a2019-09-09 20:16:54 +00004891 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004892 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004893 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004894 if not issue:
4895 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004896
Aaron Gablea718c3e2017-08-28 17:47:28 -07004897 base = cl._GitGetBranchConfigValue('last-upload-hash')
4898 if not base:
4899 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4900 if not base:
4901 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4902 revision_info = detail['revisions'][detail['current_revision']]
4903 fetch_info = revision_info['fetch']['http']
4904 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4905 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004906
Aaron Gablea718c3e2017-08-28 17:47:28 -07004907 cmd = ['git', 'diff']
4908 if options.stat:
4909 cmd.append('--stat')
4910 cmd.append(base)
4911 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004912
4913 return 0
4914
4915
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004916@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004917def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004918 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004919 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004920 '--ignore-current',
4921 action='store_true',
4922 help='Ignore the CL\'s current reviewers and start from scratch.')
4923 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004924 '--ignore-self',
4925 action='store_true',
4926 help='Do not consider CL\'s author as an owners.')
4927 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004928 '--no-color',
4929 action='store_true',
4930 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07004931 parser.add_option(
4932 '--batch',
4933 action='store_true',
4934 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00004935 # TODO: Consider moving this to another command, since other
4936 # git-cl owners commands deal with owners for a given CL.
4937 parser.add_option(
4938 '--show-all',
4939 action='store_true',
4940 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004941 options, args = parser.parse_args(args)
4942
Edward Lemur934836a2019-09-09 20:16:54 +00004943 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00004944 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004945
Yang Guo6e269a02019-06-26 11:17:02 +00004946 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00004947 if len(args) == 0:
4948 print('No files specified for --show-all. Nothing to do.')
4949 return 0
Edward Lesmese1576912021-02-16 21:53:34 +00004950 owners_by_path = cl.owners_client.BatchListOwners(args)
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +00004951 for path in args:
4952 print('Owners for %s:' % path)
4953 print('\n'.join(
4954 ' - %s' % owner
4955 for owner in owners_by_path.get(path, ['No owners found'])))
Yang Guo6e269a02019-06-26 11:17:02 +00004956 return 0
4957
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004958 if args:
4959 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004960 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004961 base_branch = args[0]
4962 else:
4963 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004964 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004965
Edward Lemur2c62b332020-03-12 22:12:33 +00004966 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07004967
4968 if options.batch:
Edward Lesmese1576912021-02-16 21:53:34 +00004969 owners = cl.owners_client.SuggestOwners(affected_files, exclude=[author])
4970 print('\n'.join(owners))
Dirk Prankebf980882017-09-02 15:08:00 -07004971 return 0
4972
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004973 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07004974 affected_files,
Edward Lemur707d70b2018-02-07 00:50:14 +01004975 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004976 [] if options.ignore_current else cl.GetReviewers(),
Edward Lesmes5cd75472021-02-19 00:34:25 +00004977 cl.owners_client,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02004978 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004979 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004980
4981
Aiden Bennerc08566e2018-10-03 17:52:42 +00004982def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004983 """Generates a diff command."""
4984 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00004985 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
4986
Aiden Benner6c18a1a2018-11-23 20:18:23 +00004987 if allow_prefix:
4988 # explicitly setting --src-prefix and --dst-prefix is necessary in the
4989 # case that diff.noprefix is set in the user's git config.
4990 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
4991 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00004992 diff_cmd += ['--no-prefix']
4993
4994 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004995
4996 if args:
4997 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004998 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004999 diff_cmd.append(arg)
5000 else:
5001 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005002
5003 return diff_cmd
5004
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005005
Jamie Madill5e96ad12020-01-13 16:08:35 +00005006def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
5007 """Runs clang-format-diff and sets a return value if necessary."""
5008
5009 if not clang_diff_files:
5010 return 0
5011
5012 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5013 # formatted. This is used to block during the presubmit.
5014 return_value = 0
5015
5016 # Locate the clang-format binary in the checkout
5017 try:
5018 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
5019 except clang_format.NotFoundError as e:
5020 DieWithError(e)
5021
5022 if opts.full or settings.GetFormatFullByDefault():
5023 cmd = [clang_format_tool]
5024 if not opts.dry_run and not opts.diff:
5025 cmd.append('-i')
5026 if opts.dry_run:
5027 for diff_file in clang_diff_files:
5028 with open(diff_file, 'r') as myfile:
5029 code = myfile.read().replace('\r\n', '\n')
5030 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5031 stdout = stdout.replace('\r\n', '\n')
5032 if opts.diff:
5033 sys.stdout.write(stdout)
5034 if code != stdout:
5035 return_value = 2
5036 else:
5037 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5038 if opts.diff:
5039 sys.stdout.write(stdout)
5040 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00005041 try:
5042 script = clang_format.FindClangFormatScriptInChromiumTree(
5043 'clang-format-diff.py')
5044 except clang_format.NotFoundError as e:
5045 DieWithError(e)
5046
Edward Lesmes89624cd2020-04-06 17:51:56 +00005047 cmd = ['vpython', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00005048 if not opts.dry_run and not opts.diff:
5049 cmd.append('-i')
5050
5051 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00005052 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00005053
Edward Lesmes89624cd2020-04-06 17:51:56 +00005054 env = os.environ.copy()
5055 env['PATH'] = (
5056 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
5057 stdout = RunCommand(
5058 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005059 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00005060 if opts.diff:
5061 sys.stdout.write(stdout)
5062 if opts.dry_run and len(stdout) > 0:
5063 return_value = 2
5064
5065 return return_value
5066
5067
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005068def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005069 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005070 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005071
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005072
enne@chromium.org555cfe42014-01-29 18:21:39 +00005073@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005074@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005075def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005076 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005077 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005078 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005079 parser.add_option('--full', action='store_true',
5080 help='Reformat the full content of all touched files')
5081 parser.add_option('--dry-run', action='store_true',
5082 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005083 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005084 '--no-clang-format',
5085 dest='clang_format',
5086 action='store_false',
5087 default=True,
5088 help='Disables formatting of various file types using clang-format.')
5089 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005090 '--python',
5091 action='store_true',
5092 default=None,
5093 help='Enables python formatting on all python files.')
5094 parser.add_option(
5095 '--no-python',
5096 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005097 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005098 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005099 'If neither --python or --no-python are set, python files that have a '
5100 '.style.yapf file in an ancestor directory will be formatted. '
5101 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005102 parser.add_option(
5103 '--js',
5104 action='store_true',
5105 help='Format javascript code with clang-format. '
5106 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005107 parser.add_option('--diff', action='store_true',
5108 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005109 parser.add_option('--presubmit', action='store_true',
5110 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005111 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005112
Garrett Beaty91a6f332020-01-06 16:57:24 +00005113 if opts.python is not None and opts.no_python:
5114 raise parser.error('Cannot set both --python and --no-python')
5115 if opts.no_python:
5116 opts.python = False
5117
Daniel Chengc55eecf2016-12-30 03:11:02 -08005118 # Normalize any remaining args against the current path, so paths relative to
5119 # the current directory are still resolved as expected.
5120 args = [os.path.join(os.getcwd(), arg) for arg in args]
5121
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005122 # git diff generates paths against the root of the repository. Change
5123 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005124 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005125 if rel_base_path:
5126 os.chdir(rel_base_path)
5127
digit@chromium.org29e47272013-05-17 17:01:46 +00005128 # Grab the merge-base commit, i.e. the upstream commit of the current
5129 # branch when it was created or the last time it was rebased. This is
5130 # to cover the case where the user may have called "git fetch origin",
5131 # moving the origin branch to a newer commit, but hasn't rebased yet.
5132 upstream_commit = None
5133 cl = Changelist()
5134 upstream_branch = cl.GetUpstreamBranch()
5135 if upstream_branch:
5136 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5137 upstream_commit = upstream_commit.strip()
5138
5139 if not upstream_commit:
5140 DieWithError('Could not find base commit for this branch. '
5141 'Are you in detached state?')
5142
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005143 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5144 diff_output = RunGit(changed_files_cmd)
5145 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005146 # Filter out files deleted by this CL
5147 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005148
Andreas Haas417d89c2020-02-06 10:24:27 +00005149 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005150 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005151
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005152 clang_diff_files = []
5153 if opts.clang_format:
5154 clang_diff_files = [
5155 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5156 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005157 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005158 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005159
Edward Lesmes50da7702020-03-30 19:23:43 +00005160 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005161
Jamie Madill5e96ad12020-01-13 16:08:35 +00005162 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5163 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005164
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005165 # Similar code to above, but using yapf on .py files rather than clang-format
5166 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005167 py_explicitly_disabled = opts.python is not None and not opts.python
5168 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005169 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5170 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005171
Aiden Bennerc08566e2018-10-03 17:52:42 +00005172 # Used for caching.
5173 yapf_configs = {}
5174 for f in python_diff_files:
5175 # Find the yapf style config for the current file, defaults to depot
5176 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005177 _FindYapfConfigFile(f, yapf_configs, top_dir)
5178
5179 # Turn on python formatting by default if a yapf config is specified.
5180 # This breaks in the case of this repo though since the specified
5181 # style file is also the global default.
5182 if opts.python is None:
5183 filtered_py_files = []
5184 for f in python_diff_files:
5185 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5186 filtered_py_files.append(f)
5187 else:
5188 filtered_py_files = python_diff_files
5189
5190 # Note: yapf still seems to fix indentation of the entire file
5191 # even if line ranges are specified.
5192 # See https://github.com/google/yapf/issues/499
5193 if not opts.full and filtered_py_files:
5194 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5195
Brian Sheedyb4307d52019-12-02 19:18:17 +00005196 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
5197 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
5198 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00005199
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005200 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005201 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
5202 # Default to pep8 if not .style.yapf is found.
5203 if not yapf_style:
5204 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00005205
Peter Wend9399922020-06-17 17:33:49 +00005206 with open(f, 'r') as py_f:
5207 if 'python3' in py_f.readline():
5208 vpython_script = 'vpython3'
5209 else:
5210 vpython_script = 'vpython'
5211
5212 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00005213
5214 has_formattable_lines = False
5215 if not opts.full:
5216 # Only run yapf over changed line ranges.
5217 for diff_start, diff_len in py_line_diffs[f]:
5218 diff_end = diff_start + diff_len - 1
5219 # Yapf errors out if diff_end < diff_start but this
5220 # is a valid line range diff for a removal.
5221 if diff_end >= diff_start:
5222 has_formattable_lines = True
5223 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5224 # If all line diffs were removals we have nothing to format.
5225 if not has_formattable_lines:
5226 continue
5227
5228 if opts.diff or opts.dry_run:
5229 cmd += ['--diff']
5230 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00005231 stdout = RunCommand(cmd,
5232 error_ok=True,
5233 cwd=top_dir,
5234 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00005235 if opts.diff:
5236 sys.stdout.write(stdout)
5237 elif len(stdout) > 0:
5238 return_value = 2
5239 else:
5240 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00005241 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005242
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005243 # Format GN build files. Always run on full build files for canonical form.
5244 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005245 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005246 if opts.dry_run or opts.diff:
5247 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005248 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005249 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005250 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07005251 cwd=top_dir)
5252 if opts.dry_run and gn_ret == 2:
5253 return_value = 2 # Not formatted.
5254 elif opts.diff and gn_ret == 2:
5255 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005256 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005257 elif gn_ret != 0:
5258 # For non-dry run cases (and non-2 return values for dry-run), a
5259 # nonzero error code indicates a failure, probably because the file
5260 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005261 DieWithError('gn format failed on ' + gn_diff_file +
5262 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005263
Ilya Shermane081cbe2017-08-15 17:51:04 -07005264 # Skip the metrics formatting from the global presubmit hook. These files have
5265 # a separate presubmit hook that issues an error if the files need formatting,
5266 # whereas the top-level presubmit script merely issues a warning. Formatting
5267 # these files is somewhat slow, so it's important not to duplicate the work.
5268 if not opts.presubmit:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005269 for diff_xml in GetDiffXMLs(diff_files):
5270 xml_dir = GetMetricsDir(diff_xml)
5271 if not xml_dir:
5272 continue
5273
Ilya Shermane081cbe2017-08-15 17:51:04 -07005274 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005275 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
5276 cmd = ['vpython', pretty_print_tool, '--non-interactive']
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005277
5278 # If the XML file is histograms.xml or enums.xml, add the xml path to the
5279 # command as histograms/pretty_print.py now needs a relative path argument
5280 # after splitting the histograms into multiple directories.
5281 # For example, in tools/metrics/ukm, pretty-print could be run using:
5282 # $ python pretty_print.py
5283 # But in tools/metrics/histogrmas, pretty-print should be run with an
5284 # additional relative path argument, like:
5285 # $ python pretty_print.py histograms_xml/UMA/histograms.xml
5286 # $ python pretty_print.py enums.xml
5287
5288 # TODO (crbug/1116488): Remove this check after ensuring that the updated
5289 # version of histograms/pretty_print.py is released.
5290 filepath_required = os.path.exists(
5291 os.path.join(tool_dir, 'validate_prefix.py'))
5292
Weilun Shib92c4b72020-08-27 17:45:11 +00005293 if (diff_xml.endswith('histograms.xml') or diff_xml.endswith('enums.xml')
5294 or diff_xml.endswith('histogram_suffixes_list.xml')
5295 ) and filepath_required:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005296 cmd.append(diff_xml)
5297
Ilya Shermane081cbe2017-08-15 17:51:04 -07005298 if opts.dry_run or opts.diff:
5299 cmd.append('--diff')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005300
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005301 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
5302 # `shell` param and instead replace `'vpython'` with
5303 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005304 stdout = RunCommand(cmd,
5305 cwd=top_dir,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005306 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07005307 if opts.diff:
5308 sys.stdout.write(stdout)
5309 if opts.dry_run and stdout:
5310 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005311
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005312 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005313
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005314
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005315def GetDiffXMLs(diff_files):
5316 return [
5317 os.path.normpath(x) for x in diff_files if MatchingFileType(x, ['.xml'])
5318 ]
5319
5320
5321def GetMetricsDir(diff_xml):
Steven Holte2e664bf2017-04-21 13:10:47 -07005322 metrics_xml_dirs = [
5323 os.path.join('tools', 'metrics', 'actions'),
5324 os.path.join('tools', 'metrics', 'histograms'),
5325 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00005326 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005327 os.path.join('tools', 'metrics', 'ukm'),
5328 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005329 for xml_dir in metrics_xml_dirs:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00005330 if diff_xml.startswith(xml_dir):
5331 return xml_dir
5332 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07005333
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005334
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005335@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005336@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005337def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005338 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005339 _, args = parser.parse_args(args)
5340
5341 if len(args) != 1:
5342 parser.print_help()
5343 return 1
5344
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005345 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005346 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005347 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005348
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005349 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005350
Edward Lemur52969c92020-02-06 18:15:28 +00005351 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00005352 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00005353 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005354
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005355 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005356 for key, issue in [x.split() for x in output.splitlines()]:
5357 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00005358 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00005359
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005360 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005362 return 1
5363 if len(branches) == 1:
5364 RunGit(['checkout', branches[0]])
5365 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005367 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005368 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00005369 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005370 try:
5371 RunGit(['checkout', branches[int(which)]])
5372 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005373 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005374 return 1
5375
5376 return 0
5377
5378
maruel@chromium.org29404b52014-09-08 22:58:00 +00005379def CMDlol(parser, args):
5380 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005381 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005382 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5383 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5384 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
Gavin Mak18f45d22020-12-04 21:45:10 +00005385 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005386 return 0
5387
5388
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005389class OptionParser(optparse.OptionParser):
5390 """Creates the option parse and add --verbose support."""
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00005391
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005392 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005393 optparse.OptionParser.__init__(
5394 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005395 self.add_option(
5396 '-v', '--verbose', action='count', default=0,
5397 help='Use 2 times for more debugging info')
5398
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005399 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005400 try:
5401 return self._parse_args(args)
5402 finally:
5403 # Regardless of success or failure of args parsing, we want to report
5404 # metrics, but only after logging has been initialized (if parsing
5405 # succeeded).
5406 global settings
5407 settings = Settings()
5408
Edward Lesmes9c349062021-05-06 20:02:39 +00005409 if metrics.collector.config.should_collect_metrics:
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005410 # GetViewVCUrl ultimately calls logging method.
5411 project_url = settings.GetViewVCUrl().strip('/+')
5412 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5413 metrics.collector.add('project_urls', [project_url])
5414
5415 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005416 # Create an optparse.Values object that will store only the actual passed
5417 # options, without the defaults.
5418 actual_options = optparse.Values()
5419 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5420 # Create an optparse.Values object with the default options.
5421 options = optparse.Values(self.get_default_values().__dict__)
5422 # Update it with the options passed by the user.
5423 options._update_careful(actual_options.__dict__)
5424 # Store the options passed by the user in an _actual_options attribute.
5425 # We store only the keys, and not the values, since the values can contain
5426 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005427 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005428
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005429 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005430 logging.basicConfig(
5431 level=levels[min(options.verbose, len(levels) - 1)],
5432 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5433 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005434
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005435 return options, args
5436
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005437
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005438def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005439 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005440 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005441 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005442 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005443
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005444 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005445 dispatcher = subcommand.CommandDispatcher(__name__)
5446 try:
5447 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005448 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005449 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005450 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005451 if e.code != 500:
5452 raise
5453 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005454 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005455 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005456 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005457
5458
5459if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005460 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5461 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005462 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005463 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005464 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005465 sys.exit(main(sys.argv[1:]))