blob: da2b4bc30f797cfc11d532cea383bab26908f797 [file] [log] [blame]
Edward Lemur1f3bafb2019-10-08 17:56:33 +00001#!/usr/bin/env vpython
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010014import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000015import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000016import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010017import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010024import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070027import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000029import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000035import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000039import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000041import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000042import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000043import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000044import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000045import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000047import presubmit_canned_checks
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048import presubmit_support
49import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000050import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040051import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000052import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import watchlists
55
Edward Lemur79d4f992019-11-11 23:49:02 +000056from third_party import six
57from six.moves import urllib
58
59
60if sys.version_info.major == 3:
61 basestring = (str,) # pylint: disable=redefined-builtin
62
Edward Lemurb9830242019-10-30 22:19:20 +000063
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
Edward Lemur0f58ae42019-04-30 17:24:12 +000066# Traces for git push will be stored in a traces directory inside the
67# depot_tools checkout.
68DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
69TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000070PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000071
72# When collecting traces, Git hashes will be reduced to 6 characters to reduce
73# the size after compression.
74GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
75# Used to redact the cookies from the gitcookies file.
76GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
77
Edward Lemurd4d1ba42019-09-20 21:46:37 +000078MAX_ATTEMPTS = 3
79
Edward Lemur1b52d872019-05-09 21:12:12 +000080# The maximum number of traces we will keep. Multiplied by 3 since we store
81# 3 files per trace.
82MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000083# Message to be displayed to the user to inform where to find the traces for a
84# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000085TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000086'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000087'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000088' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000089'Copies of your gitcookies file and git config have been recorded at:\n'
90' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000091# Format of the message to be stored as part of the traces to give developers a
92# better context when they go through traces.
93TRACES_README_FORMAT = (
94'Date: %(now)s\n'
95'\n'
96'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
97'Title: %(title)s\n'
98'\n'
99'%(description)s\n'
100'\n'
101'Execution time: %(execution_time)s\n'
102'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000103
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800104POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000105DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000106REFS_THAT_ALIAS_TO_OTHER_REFS = {
107 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
108 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
109}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
thestig@chromium.org44202a22014-03-11 19:22:18 +0000111# Valid extensions for files we want to lint.
112DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
113DEFAULT_LINT_IGNORE_REGEX = r"$^"
114
Aiden Bennerc08566e2018-10-03 17:52:42 +0000115# File name for yapf style config files.
116YAPF_CONFIG_FILENAME = '.style.yapf'
117
Edward Lesmes50da7702020-03-30 19:23:43 +0000118# The issue, patchset and codereview server are stored on git config for each
119# branch under branch.<branch-name>.<config-key>.
120ISSUE_CONFIG_KEY = 'gerritissue'
121PATCHSET_CONFIG_KEY = 'gerritpatchset'
122CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
123
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000124# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000125Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000126
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000127# Initialized in main()
128settings = None
129
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100130# Used by tests/git_cl_test.py to add extra logging.
131# Inside the weirdly failing test, add this:
132# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700133# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100134_IS_BEING_TESTED = False
135
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000136
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000137_KNOWN_GERRIT_TO_SHORT_URLS = {
138 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
139 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
140}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000141assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
142 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000143
144
Christopher Lamf732cd52017-01-24 12:40:11 +1100145def DieWithError(message, change_desc=None):
146 if change_desc:
147 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000148 print('\n ** Content of CL description **\n' +
149 '='*72 + '\n' +
150 change_desc.description + '\n' +
151 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100152
vapiera7fbd5a2016-06-16 09:17:49 -0700153 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000154 sys.exit(1)
155
156
Christopher Lamf732cd52017-01-24 12:40:11 +1100157def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000158 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000159 print('\nsaving CL description to %s\n' % backup_path)
Josip906bfde2020-01-31 22:38:49 +0000160 with open(backup_path, 'w') as backup_file:
161 backup_file.write(change_desc.description)
Christopher Lamf732cd52017-01-24 12:40:11 +1100162
163
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000164def GetNoGitPagerEnv():
165 env = os.environ.copy()
166 # 'cat' is a magical git string that disables pagers on all platforms.
167 env['GIT_PAGER'] = 'cat'
168 return env
169
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000170
bsep@chromium.org627d9002016-04-29 00:00:52 +0000171def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000172 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000173 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
174 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000175 except subprocess2.CalledProcessError as e:
176 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000177 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000178 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000179 'Command "%s" failed.\n%s' % (
180 ' '.join(args), error_message or e.stdout or ''))
Edward Lemur79d4f992019-11-11 23:49:02 +0000181 return e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000182
183
184def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000185 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000186 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000187
188
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000189def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000190 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700191 if suppress_stderr:
192 stderr = subprocess2.VOID
193 else:
194 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000195 try:
tandrii5d48c322016-08-18 16:19:37 -0700196 (out, _), code = subprocess2.communicate(['git'] + args,
197 env=GetNoGitPagerEnv(),
198 stdout=subprocess2.PIPE,
199 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000200 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700201 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900202 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000203 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000204
205
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000206def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000207 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000208 return RunGitWithCode(args, suppress_stderr=True)[1]
209
210
tandrii2a16b952016-10-19 07:09:44 -0700211def time_sleep(seconds):
212 # Use this so that it can be mocked in tests without interfering with python
213 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700214 return time.sleep(seconds)
215
216
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000217def time_time():
218 # Use this so that it can be mocked in tests without interfering with python
219 # system machinery.
220 return time.time()
221
222
Edward Lemur1b52d872019-05-09 21:12:12 +0000223def datetime_now():
224 # Use this so that it can be mocked in tests without interfering with python
225 # system machinery.
226 return datetime.datetime.now()
227
228
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100229def confirm_or_exit(prefix='', action='confirm'):
230 """Asks user to press enter to continue or press Ctrl+C to abort."""
231 if not prefix or prefix.endswith('\n'):
232 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100233 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100234 mid = ' Press'
235 elif prefix.endswith(' '):
236 mid = 'press'
237 else:
238 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000239 gclient_utils.AskForData(
240 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100241
242
243def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000244 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000245 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100246 while True:
247 if 'yes'.startswith(result):
248 return True
249 if 'no'.startswith(result):
250 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000251 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100252
253
machenbach@chromium.org45453142015-09-15 08:45:22 +0000254def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000255 prop_list = getattr(options, 'properties', [])
256 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000257 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000258 try:
259 properties[key] = json.loads(val)
260 except ValueError:
261 pass # If a value couldn't be evaluated, treat it as a string.
262 return properties
263
264
Edward Lemur4c707a22019-09-24 21:13:43 +0000265def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000266 """Calls a buildbucket v2 method and returns the parsed json response."""
267 headers = {
268 'Accept': 'application/json',
269 'Content-Type': 'application/json',
270 }
271 request = json.dumps(request)
272 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
273
274 logging.info('POST %s with %s' % (url, request))
275
276 attempts = 1
277 time_to_sleep = 1
278 while True:
279 response, content = http.request(url, 'POST', body=request, headers=headers)
280 if response.status == 200:
281 return json.loads(content[4:])
282 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
283 msg = '%s error when calling POST %s with %s: %s' % (
284 response.status, url, request, content)
285 raise BuildbucketResponseException(msg)
286 logging.debug(
287 '%s error when calling POST %s with %s. '
288 'Sleeping for %d seconds and retrying...' % (
289 response.status, url, request, time_to_sleep))
290 time.sleep(time_to_sleep)
291 time_to_sleep *= 2
292 attempts += 1
293
294 assert False, 'unreachable'
295
296
Edward Lemur6215c792019-10-03 21:59:05 +0000297def _parse_bucket(raw_bucket):
298 legacy = True
299 project = bucket = None
300 if '/' in raw_bucket:
301 legacy = False
302 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000303 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000304 elif raw_bucket.startswith('luci.'):
305 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000306 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000307 elif '.' in raw_bucket:
308 project = raw_bucket.split('.')[0]
309 bucket = raw_bucket
310 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000311 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000312 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
313 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000314
315
Quinten Yearsley777660f2020-03-04 23:37:06 +0000316def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000317 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700318
319 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000320 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000321 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700322 options: Command-line options.
323 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000324 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000325 for project, bucket, builder in jobs:
326 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000327 print('To see results here, run: git cl try-results')
328 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700329
Quinten Yearsley777660f2020-03-04 23:37:06 +0000330 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000331 if not requests:
332 return
333
Edward Lemur5b929a42019-10-21 17:57:39 +0000334 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000335 http.force_exception_to_status_code = True
336
337 batch_request = {'requests': requests}
338 batch_response = _call_buildbucket(
339 http, options.buildbucket_host, 'Batch', batch_request)
340
341 errors = [
342 ' ' + response['error']['message']
343 for response in batch_response.get('responses', [])
344 if 'error' in response
345 ]
346 if errors:
347 raise BuildbucketResponseException(
348 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
349
350
Quinten Yearsley777660f2020-03-04 23:37:06 +0000351def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000352 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000353 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000354 shared_properties = {
355 'category': options.ensure_value('category', 'git_cl_try')
356 }
357 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000358 shared_properties['clobber'] = True
359 shared_properties.update(_get_properties_from_options(options) or {})
360
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000361 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000362 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000363 shared_tags.append({'key': 'retry_failed',
364 'value': '1'})
365
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000366 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000367 for (project, bucket, builder) in jobs:
368 properties = shared_properties.copy()
369 if 'presubmit' in builder.lower():
370 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000371
Edward Lemur45768512020-03-02 19:03:14 +0000372 requests.append({
373 'scheduleBuild': {
374 'requestId': str(uuid.uuid4()),
375 'builder': {
376 'project': getattr(options, 'project', None) or project,
377 'bucket': bucket,
378 'builder': builder,
379 },
380 'gerritChanges': gerrit_changes,
381 'properties': properties,
382 'tags': [
383 {'key': 'builder', 'value': builder},
384 ] + shared_tags,
385 }
386 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000387
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000388 if options.ensure_value('revision', None):
Edward Lemur45768512020-03-02 19:03:14 +0000389 requests[-1]['scheduleBuild']['gitilesCommit'] = {
390 'host': gerrit_changes[0]['host'],
391 'project': gerrit_changes[0]['project'],
392 'id': options.revision
393 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000394
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000395 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
Quinten Yearsley777660f2020-03-04 23:37:06 +0000398def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000399 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000401 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000402 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000403 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000404 request = {
405 'predicate': {
406 'gerritChanges': [changelist.GetGerritChange(patchset)],
407 },
408 'fields': ','.join('builds.*.' + field for field in fields),
409 }
tandrii221ab252016-10-06 08:12:04 -0700410
Edward Lemur5b929a42019-10-21 17:57:39 +0000411 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000412 if authenticator.has_cached_credentials():
413 http = authenticator.authorize(httplib2.Http())
414 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700415 print('Warning: Some results might be missing because %s' %
416 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000417 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000418 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 http.force_exception_to_status_code = True
420
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000421 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
422 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000423
Edward Lemur45768512020-03-02 19:03:14 +0000424
Edward Lemur5b929a42019-10-21 17:57:39 +0000425def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000426 """Fetches builds from the latest patchset that has builds (within
427 the last few patchsets).
428
429 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000430 changelist (Changelist): The CL to fetch builds for
431 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000432 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
433 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000434 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000435 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
436 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000437 """
438 assert buildbucket_host
439 assert changelist.GetIssue(), 'CL must be uploaded first'
440 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000441 if latest_patchset is None:
442 assert changelist.GetMostRecentPatchset()
443 ps = changelist.GetMostRecentPatchset()
444 else:
445 assert latest_patchset > 0, latest_patchset
446 ps = latest_patchset
447
Quinten Yearsley983111f2019-09-26 17:18:48 +0000448 min_ps = max(1, ps - 5)
449 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000450 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000451 if len(builds):
452 return builds, ps
453 ps -= 1
454 return [], 0
455
456
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000457def _filter_failed_for_retry(all_builds):
458 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000459
460 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000461 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000462 i.e. a list of buildbucket.v2.Builds which includes status and builder
463 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000464
465 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000466 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000467 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000468 """
Edward Lemur45768512020-03-02 19:03:14 +0000469 grouped = {}
470 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000471 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000472 key = (builder['project'], builder['bucket'], builder['builder'])
473 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000474
Edward Lemur45768512020-03-02 19:03:14 +0000475 jobs = []
476 for (project, bucket, builder), builds in grouped.items():
477 if 'triggered' in builder:
478 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
479 'from a parent. Please schedule a manual job for the parent '
480 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000481 continue
482 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
483 # Don't retry if any are running.
484 continue
Edward Lemur45768512020-03-02 19:03:14 +0000485 # If builder had several builds, retry only if the last one failed.
486 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
487 # build, but in case of retrying failed jobs retrying a flaky one makes
488 # sense.
489 builds = sorted(builds, key=lambda b: b['createTime'])
490 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
491 continue
492 # Don't retry experimental build previously triggered by CQ.
493 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
494 for t in builds[-1]['tags']):
495 continue
496 jobs.append((project, bucket, builder))
497
498 # Sort the jobs to make testing easier.
499 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000500
501
Quinten Yearsley777660f2020-03-04 23:37:06 +0000502def _print_tryjobs(options, builds):
503 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000505 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000506 return
507
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000508 longest_builder = max(len(b['builder']['builder']) for b in builds)
509 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000511 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
512 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000514 builds_by_status = {}
515 for b in builds:
516 builds_by_status.setdefault(b['status'], []).append({
517 'id': b['id'],
518 'name': name_fmt.format(
519 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
520 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000522 sort_key = lambda b: (b['name'], b['id'])
523
524 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000526 if not builds:
527 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000529 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000530 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000531 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532 else:
533 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
534
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000535 print(colorize(title))
536 for b in sorted(builds, key=sort_key):
537 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538
539 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000540 print_builds(
541 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
542 print_builds(
543 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
544 color=Fore.MAGENTA)
545 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
546 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
547 color=Fore.MAGENTA)
548 print_builds('Started:', builds_by_status.pop('STARTED', []))
549 print_builds(
550 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000552 print_builds(
553 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000554 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555
556
Aiden Bennerc08566e2018-10-03 17:52:42 +0000557def _ComputeDiffLineRanges(files, upstream_commit):
558 """Gets the changed line ranges for each file since upstream_commit.
559
560 Parses a git diff on provided files and returns a dict that maps a file name
561 to an ordered list of range tuples in the form (start_line, count).
562 Ranges are in the same format as a git diff.
563 """
564 # If files is empty then diff_output will be a full diff.
565 if len(files) == 0:
566 return {}
567
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000568 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000569 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000570 diff_output = RunGit(diff_cmd)
571
572 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
573 # 2 capture groups
574 # 0 == fname of diff file
575 # 1 == 'diff_start,diff_count' or 'diff_start'
576 # will match each of
577 # diff --git a/foo.foo b/foo.py
578 # @@ -12,2 +14,3 @@
579 # @@ -12,2 +17 @@
580 # running re.findall on the above string with pattern will give
581 # [('foo.py', ''), ('', '14,3'), ('', '17')]
582
583 curr_file = None
584 line_diffs = {}
585 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
586 if match[0] != '':
587 # Will match the second filename in diff --git a/a.py b/b.py.
588 curr_file = match[0]
589 line_diffs[curr_file] = []
590 else:
591 # Matches +14,3
592 if ',' in match[1]:
593 diff_start, diff_count = match[1].split(',')
594 else:
595 # Single line changes are of the form +12 instead of +12,1.
596 diff_start = match[1]
597 diff_count = 1
598
599 diff_start = int(diff_start)
600 diff_count = int(diff_count)
601
602 # If diff_count == 0 this is a removal we can ignore.
603 line_diffs[curr_file].append((diff_start, diff_count))
604
605 return line_diffs
606
607
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000608def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000609 """Checks if a yapf file is in any parent directory of fpath until top_dir.
610
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000611 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000612 is found returns None. Uses yapf_config_cache as a cache for previously found
613 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000614 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000615 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000616 # Return result if we've already computed it.
617 if fpath in yapf_config_cache:
618 return yapf_config_cache[fpath]
619
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000620 parent_dir = os.path.dirname(fpath)
621 if os.path.isfile(fpath):
622 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000623 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000624 # Otherwise fpath is a directory
625 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
626 if os.path.isfile(yapf_file):
627 ret = yapf_file
628 elif fpath == top_dir or parent_dir == fpath:
629 # If we're at the top level directory, or if we're at root
630 # there is no provided style.
631 ret = None
632 else:
633 # Otherwise recurse on the current directory.
634 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000635 yapf_config_cache[fpath] = ret
636 return ret
637
638
Brian Sheedyb4307d52019-12-02 19:18:17 +0000639def _GetYapfIgnorePatterns(top_dir):
640 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000641
642 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
643 but this functionality appears to break when explicitly passing files to
644 yapf for formatting. According to
645 https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
646 the .yapfignore file should be in the directory that yapf is invoked from,
647 which we assume to be the top level directory in this case.
648
649 Args:
650 top_dir: The top level directory for the repository being formatted.
651
652 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000653 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000654 """
655 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000656 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000657 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000658 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000659
Brian Sheedyb4307d52019-12-02 19:18:17 +0000660 with open(yapfignore_file) as f:
661 for line in f.readlines():
662 stripped_line = line.strip()
663 # Comments and blank lines should be ignored.
664 if stripped_line.startswith('#') or stripped_line == '':
665 continue
666 ignore_patterns.add(stripped_line)
667 return ignore_patterns
668
669
670def _FilterYapfIgnoredFiles(filepaths, patterns):
671 """Filters out any filepaths that match any of the given patterns.
672
673 Args:
674 filepaths: An iterable of strings containing filepaths to filter.
675 patterns: An iterable of strings containing fnmatch patterns to filter on.
676
677 Returns:
678 A list of strings containing all the elements of |filepaths| that did not
679 match any of the patterns in |patterns|.
680 """
681 # Not inlined so that tests can use the same implementation.
682 return [f for f in filepaths
683 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000684
685
Aaron Gable13101a62018-02-09 13:20:41 -0800686def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000687 """Prints statistics about the change to the user."""
688 # --no-ext-diff is broken in some versions of Git, so try to work around
689 # this by overriding the environment (but there is still a problem if the
690 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000691 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000692 if 'GIT_EXTERNAL_DIFF' in env:
693 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000694
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000695 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800696 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000697 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000698
699
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000700class BuildbucketResponseException(Exception):
701 pass
702
703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000704class Settings(object):
705 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000707 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 self.tree_status_url = None
709 self.viewvc_url = None
710 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000711 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000712 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000713 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000714 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000715 self.format_full_by_default = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716
Edward Lemur26964072020-02-19 19:18:51 +0000717 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000719 if self.updated:
720 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000721
Edward Lemur26964072020-02-19 19:18:51 +0000722 # The only value that actually changes the behavior is
723 # autoupdate = "false". Everything else means "true".
724 autoupdate = (
725 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
726
727 cr_settings_file = FindCodereviewSettingsFile()
728 if autoupdate != 'false' and cr_settings_file:
729 LoadCodereviewSettingsFromFile(cr_settings_file)
730 cr_settings_file.close()
731
732 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000734 @staticmethod
735 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000736 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000739 if self.root is None:
740 self.root = os.path.abspath(self.GetRelativeRoot())
741 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 def GetTreeStatusUrl(self, error_ok=False):
744 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000745 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
746 if self.tree_status_url is None and not error_ok:
747 DieWithError(
748 'You must configure your tree status URL by running '
749 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750 return self.tree_status_url
751
752 def GetViewVCUrl(self):
753 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000754 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 return self.viewvc_url
756
rmistry@google.com90752582014-01-14 21:04:50 +0000757 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000758 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000759
rmistry@google.com5626a922015-02-26 14:03:30 +0000760 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000761 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000762 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000763 return run_post_upload_hook == "True"
764
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000765 def GetDefaultCCList(self):
Edward Lemur26964072020-02-19 19:18:51 +0000766 return self._GetConfig('rietveld.cc')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000767
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000768 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000769 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000771 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
772 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000773 # Default is squash now (http://crbug.com/611892#c23).
774 self.squash_gerrit_uploads = self._GetConfig(
775 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000776 return self.squash_gerrit_uploads
777
Edward Lesmes4de54132020-05-05 19:41:33 +0000778 def GetSquashGerritUploadsOverride(self):
779 """Return True or False if codereview.settings should be overridden.
780
781 Returns None if no override has been defined.
782 """
783 # See also http://crbug.com/611892#c23
784 result = self._GetConfig('gerrit.override-squash-uploads').lower()
785 if result == 'true':
786 return True
787 if result == 'false':
788 return False
789 return None
790
tandrii@chromium.org28253532016-04-14 13:46:56 +0000791 def GetGerritSkipEnsureAuthenticated(self):
792 """Return True if EnsureAuthenticated should not be done for Gerrit
793 uploads."""
794 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000795 self.gerrit_skip_ensure_authenticated = self._GetConfig(
796 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000797 return self.gerrit_skip_ensure_authenticated
798
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000799 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000800 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000801 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000802 # Git requires single quotes for paths with spaces. We need to replace
803 # them with double quotes for Windows to treat such paths as a single
804 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000805 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000806 return self.git_editor or None
807
thestig@chromium.org44202a22014-03-11 19:22:18 +0000808 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000809 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000810
811 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000812 return self._GetConfig(
813 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000814
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000815 def GetFormatFullByDefault(self):
816 if self.format_full_by_default is None:
817 result = (
818 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
819 error_ok=True).strip())
820 self.format_full_by_default = (result == 'true')
821 return self.format_full_by_default
822
Edward Lemur26964072020-02-19 19:18:51 +0000823 def _GetConfig(self, key, default=''):
824 self._LazyUpdateIfNeeded()
825 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
827
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000828class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000829 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000830 NONE = 'none'
831 DRY_RUN = 'dry_run'
832 COMMIT = 'commit'
833
834 ALL_STATES = [NONE, DRY_RUN, COMMIT]
835
836
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000837class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000838 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000839 self.issue = issue
840 self.patchset = patchset
841 self.hostname = hostname
842
843 @property
844 def valid(self):
845 return self.issue is not None
846
847
Edward Lemurf38bc172019-09-03 21:02:13 +0000848def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000849 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
850 fail_result = _ParsedIssueNumberArgument()
851
Edward Lemur678a6842019-10-03 22:25:05 +0000852 if isinstance(arg, int):
853 return _ParsedIssueNumberArgument(issue=arg)
854 if not isinstance(arg, basestring):
855 return fail_result
856
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000857 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000858 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000859 if not arg.startswith('http'):
860 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700861
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000862 url = gclient_utils.UpgradeToHttps(arg)
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000863 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
864 if url.startswith(short_url):
865 url = gerrit_url + url[len(short_url):]
866 break
867
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000868 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000869 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000870 except ValueError:
871 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200872
Edward Lemur678a6842019-10-03 22:25:05 +0000873 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
874 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
875 # Short urls like https://domain/<issue_number> can be used, but don't allow
876 # specifying the patchset (you'd 404), but we allow that here.
877 if parsed_url.path == '/':
878 part = parsed_url.fragment
879 else:
880 part = parsed_url.path
881
882 match = re.match(
883 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
884 if not match:
885 return fail_result
886
887 issue = int(match.group('issue'))
888 patchset = match.group('patchset')
889 return _ParsedIssueNumberArgument(
890 issue=issue,
891 patchset=int(patchset) if patchset else None,
892 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000893
894
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000895def _create_description_from_log(args):
896 """Pulls out the commit log to use as a base for the CL description."""
897 log_args = []
898 if len(args) == 1 and not args[0].endswith('.'):
899 log_args = [args[0] + '..']
900 elif len(args) == 1 and args[0].endswith('...'):
901 log_args = [args[0][:-1]]
902 elif len(args) == 2:
903 log_args = [args[0] + '..' + args[1]]
904 else:
905 log_args = args[:] # Hope for the best!
Manh Nguyen77463bb2020-06-11 17:26:12 +0000906 return RunGit(['log', '--pretty=format:%B'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000907
908
Aaron Gablea45ee112016-11-22 15:14:38 -0800909class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700910 def __init__(self, issue, url):
911 self.issue = issue
912 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800913 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700914
915 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800916 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700917 self.issue, self.url)
918
919
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100920_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +0000921 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100922 # TODO(tandrii): these two aren't known in Gerrit.
923 'approval', 'disapproval'])
924
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000927 """Changelist works with one changelist in local branch.
928
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000929 Notes:
930 * Not safe for concurrent multi-{thread,process} use.
931 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700932 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 """
934
Edward Lemur125d60a2019-09-13 18:25:41 +0000935 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000936 """Create a new ChangeList instance.
937
Edward Lemurf38bc172019-09-03 21:02:13 +0000938 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000939 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000941 global settings
942 if not settings:
943 # Happens when git_cl.py is used as a utility library.
944 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.branchref = branchref
947 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000948 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +0000949 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 else:
951 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000953 self.lookedup_issue = False
954 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000958 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -0800959 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000960 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +0000961 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000962
Edward Lemur125d60a2019-09-13 18:25:41 +0000963 # Lazily cached values.
964 self._gerrit_host = None # e.g. chromium-review.googlesource.com
965 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
966 # Map from change number (issue) to its detail cache.
967 self._detail_cache = {}
968
969 if codereview_host is not None:
970 assert not codereview_host.startswith('https://'), codereview_host
971 self._gerrit_host = codereview_host
972 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000973
974 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700975 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700977 The return value is a string suitable for passing to git cl with the --cc
978 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000979 """
980 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000981 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -0800982 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000983 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
984 return self.cc
985
Daniel Cheng7227d212017-11-17 08:12:37 -0800986 def ExtendCC(self, more_cc):
987 """Extends the list of users to cc on this CL based on the changed files."""
988 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990 def GetBranch(self):
991 """Returns the short branch name, e.g. 'master'."""
992 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +0000993 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000994 if not branchref:
995 return None
996 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +0000997 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 return self.branch
999
1000 def GetBranchRef(self):
1001 """Returns the full branch name, e.g. 'refs/heads/master'."""
1002 self.GetBranch() # Poke the lazy loader.
1003 return self.branchref
1004
Edward Lemur85153282020-02-14 22:06:29 +00001005 def _GitGetBranchConfigValue(self, key, default=None):
1006 return scm.GIT.GetBranchConfig(
1007 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001008
Edward Lemur85153282020-02-14 22:06:29 +00001009 def _GitSetBranchConfigValue(self, key, value):
1010 action = 'set %s to %r' % (key, value)
1011 if not value:
1012 action = 'unset %s' % key
1013 assert self.GetBranch(), 'a branch is needed to ' + action
1014 return scm.GIT.SetBranchConfig(
1015 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001016
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001017 @staticmethod
1018 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001019 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 e.g. 'origin', 'refs/heads/master'
1021 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001022 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1023 settings.GetRoot(), branch)
1024 if not remote or not upstream_branch:
1025 DieWithError(
1026 'Unable to determine default branch to diff against.\n'
1027 'Either pass complete "git diff"-style arguments, like\n'
1028 ' git cl upload origin/master\n'
1029 'or verify this branch is set up to track another \n'
1030 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
1032 return remote, upstream_branch
1033
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001034 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001035 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001036 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001037 DieWithError('The upstream for the current branch (%s) does not exist '
1038 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001039 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001040 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 def GetUpstreamBranch(self):
1043 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001044 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001045 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001046 upstream_branch = upstream_branch.replace('refs/heads/',
1047 'refs/remotes/%s/' % remote)
1048 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1049 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 self.upstream_branch = upstream_branch
1051 return self.upstream_branch
1052
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001053 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001054 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 remote, branch = None, self.GetBranch()
1056 seen_branches = set()
1057 while branch not in seen_branches:
1058 seen_branches.add(branch)
1059 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001060 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 if remote != '.' or branch.startswith('refs/remotes'):
1062 break
1063 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001064 remotes = RunGit(['remote'], error_ok=True).split()
1065 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001066 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001067 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001069 logging.warn('Could not determine which remote this change is '
1070 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001071 else:
1072 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001073 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 branch = 'HEAD'
1075 if branch.startswith('refs/remotes'):
1076 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001077 elif branch.startswith('refs/branch-heads/'):
1078 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001079 else:
1080 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001081 return self._remote
1082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 def GetRemoteUrl(self):
1084 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1085
1086 Returns None if there is no remote.
1087 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001088 is_cached, value = self._cached_remote_url
1089 if is_cached:
1090 return value
1091
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001092 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001093 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001094
Edward Lemur298f2cf2019-02-22 21:40:39 +00001095 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001096 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001097 if host:
1098 self._cached_remote_url = (True, url)
1099 return url
1100
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001101 # If it cannot be parsed as an url, assume it is a local directory,
1102 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001103 logging.warning('"%s" doesn\'t appear to point to a git host. '
1104 'Interpreting it as a local directory.', url)
1105 if not os.path.isdir(url):
1106 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001107 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1108 'but it doesn\'t exist.',
1109 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001110 return None
1111
1112 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001113 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001114
Edward Lemur79d4f992019-11-11 23:49:02 +00001115 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001116 if not host:
1117 logging.error(
1118 'Remote "%(remote)s" for branch "%(branch)s" points to '
1119 '"%(cache_path)s", but it is misconfigured.\n'
1120 '"%(cache_path)s" must be a git repo and must have a remote named '
1121 '"%(remote)s" pointing to the git host.', {
1122 'remote': remote,
1123 'cache_path': cache_path,
1124 'branch': self.GetBranch()})
1125 return None
1126
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001127 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001128 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001130 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001131 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001132 if self.issue is None and not self.lookedup_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00001133 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001134 if self.issue is not None:
1135 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 return self.issue
1138
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001139 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 issue = self.GetIssue()
1142 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001143 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001144 server = self.GetCodereviewServer()
1145 if short:
1146 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1147 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
Edward Lemur6c6827c2020-02-06 21:15:18 +00001149 def FetchDescription(self, pretty=False):
1150 assert self.GetIssue(), 'issue is required to query Gerrit'
1151
Edward Lemur9aa1a962020-02-25 00:58:38 +00001152 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001153 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1154 current_rev = data['current_revision']
1155 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001156
1157 if not pretty:
1158 return self.description
1159
1160 # Set width to 72 columns + 2 space indent.
1161 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1162 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1163 lines = self.description.splitlines()
1164 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
1166 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001167 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001168 if self.patchset is None and not self.lookedup_patchset:
Edward Lesmes50da7702020-03-30 19:23:43 +00001169 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001170 if self.patchset is not None:
1171 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 return self.patchset
1174
Edward Lemur9aa1a962020-02-25 00:58:38 +00001175 def GetAuthor(self):
1176 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001179 """Set this branch's patchset. If patchset=0, clears the patchset."""
1180 assert self.GetBranch()
1181 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001182 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001183 else:
1184 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001185 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001187 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001188 """Set this branch's issue. If issue isn't given, clears the issue."""
1189 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001191 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001192 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001193 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001194 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001196 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001197 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 else:
tandrii5d48c322016-08-18 16:19:37 -07001199 # Reset all of these just to be clean.
1200 reset_suffixes = [
1201 'last-upload-hash',
Edward Lesmes50da7702020-03-30 19:23:43 +00001202 ISSUE_CONFIG_KEY,
1203 PATCHSET_CONFIG_KEY,
1204 CODEREVIEW_SERVER_CONFIG_KEY,
1205 'gerritsquashhash',
1206 ]
tandrii5d48c322016-08-18 16:19:37 -07001207 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001208 try:
1209 self._GitSetBranchConfigValue(prop, None)
1210 except subprocess2.CalledProcessError:
1211 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001212 msg = RunGit(['log', '-1', '--format=%B']).strip()
1213 if msg and git_footers.get_footer_change_id(msg):
1214 print('WARNING: The change patched into this branch has a Change-Id. '
1215 'Removing it.')
1216 RunGit(['commit', '--amend', '-m',
1217 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001218 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001219 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001220 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221
Edward Lemur2c62b332020-03-12 22:12:33 +00001222 def GetAffectedFiles(self, upstream):
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001223 try:
Edward Lemur2c62b332020-03-12 22:12:33 +00001224 return [f for _, f in scm.GIT.CaptureStatus(settings.GetRoot(), upstream)]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001225 except subprocess2.CalledProcessError:
1226 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001227 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001228 'This branch probably doesn\'t exist anymore. To reset the\n'
1229 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001230 ' git branch --set-upstream-to origin/master %s\n'
1231 'or replace origin/master with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001232 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001233
dsansomee2d6fd92016-09-08 00:10:47 -07001234 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001235 assert self.GetIssue(), 'issue is required to update description'
1236
1237 if gerrit_util.HasPendingChangeEdit(
1238 self._GetGerritHost(), self._GerritChangeIdentifier()):
1239 if not force:
1240 confirm_or_exit(
1241 'The description cannot be modified while the issue has a pending '
1242 'unpublished edit. Either publish the edit in the Gerrit web UI '
1243 'or delete it.\n\n', action='delete the unpublished edit')
1244
1245 gerrit_util.DeletePendingChangeEdit(
1246 self._GetGerritHost(), self._GerritChangeIdentifier())
1247 gerrit_util.SetCommitMessage(
1248 self._GetGerritHost(), self._GerritChangeIdentifier(),
1249 description, notify='NONE')
1250
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001251 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001252
Edward Lemur75526302020-02-27 22:31:05 +00001253 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001254 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001255 '--root', settings.GetRoot(),
1256 '--upstream', upstream,
1257 ]
1258
1259 args.extend(['--verbose'] * verbose)
1260
Edward Lemur99df04e2020-03-05 19:39:43 +00001261 author = self.GetAuthor()
1262 gerrit_url = self.GetCodereviewServer()
Edward Lemur227d5102020-02-25 23:45:35 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001265 if author:
1266 args.extend(['--author', author])
1267 if gerrit_url:
1268 args.extend(['--gerrit_url', gerrit_url])
Edward Lemur227d5102020-02-25 23:45:35 +00001269 if issue:
1270 args.extend(['--issue', str(issue)])
1271 if patchset:
1272 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001273
Edward Lemur75526302020-02-27 22:31:05 +00001274 return args
1275
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001276 def RunHook(self, committing, may_prompt, verbose, parallel, upstream,
1277 description, all_files, resultdb=False):
Edward Lemur75526302020-02-27 22:31:05 +00001278 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1279 args = self._GetCommonPresubmitArgs(verbose, upstream)
1280 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001281 if may_prompt:
1282 args.append('--may_prompt')
1283 if parallel:
1284 args.append('--parallel')
1285 if all_files:
1286 args.append('--all_files')
1287
1288 with gclient_utils.temporary_file() as description_file:
1289 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001290
1291 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001292 args.extend(['--json_output', json_output])
1293 args.extend(['--description_file', description_file])
1294
1295 start = time_time()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001296
1297 cmd = ['vpython', PRESUBMIT_SUPPORT] + args
1298 if resultdb:
1299 cmd = ['rdb', 'stream', '-new'] + cmd
1300
1301 p = subprocess2.Popen(cmd)
Edward Lemur227d5102020-02-25 23:45:35 +00001302 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001303
Edward Lemur227d5102020-02-25 23:45:35 +00001304 metrics.collector.add_repeated('sub_commands', {
1305 'command': 'presubmit',
1306 'execution_time': time_time() - start,
1307 'exit_code': exit_code,
1308 })
1309
1310 if exit_code:
1311 sys.exit(exit_code)
1312
1313 json_results = gclient_utils.FileRead(json_output)
1314 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001315
Edward Lemur75526302020-02-27 22:31:05 +00001316 def RunPostUploadHook(self, verbose, upstream, description):
1317 args = self._GetCommonPresubmitArgs(verbose, upstream)
1318 args.append('--post_upload')
1319
1320 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001321 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001322 args.extend(['--description_file', description_file])
1323 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1324 p.wait()
1325
Edward Lemur5a644f82020-03-18 16:44:57 +00001326 def _GetDescriptionForUpload(self, options, git_diff_args, files):
1327 # Get description message for upload.
1328 if self.GetIssue():
1329 description = self.FetchDescription()
1330 elif options.message:
1331 description = options.message
1332 else:
1333 description = _create_description_from_log(git_diff_args)
1334 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001335 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001336
1337 # Extract bug number from branch name.
1338 bug = options.bug
1339 fixed = options.fixed
1340 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
1341 self.GetBranch())
1342 if not bug and not fixed and match:
1343 if match.group('type') == 'bug':
1344 bug = match.group('bugnum')
1345 else:
1346 fixed = match.group('bugnum')
1347
1348 change_description = ChangeDescription(description, bug, fixed)
1349
1350 # Set the reviewer list now so that presubmit checks can access it.
1351 if options.reviewers or options.tbrs or options.add_owners_to:
1352 change_description.update_reviewers(
1353 options.reviewers, options.tbrs, options.add_owners_to, files,
1354 self.GetAuthor())
1355
1356 return change_description
1357
1358 def _GetTitleForUpload(self, options):
1359 # When not squashing, just return options.title.
1360 if not options.squash:
1361 return options.title
1362
1363 # On first upload, patchset title is always this string, while options.title
1364 # gets converted to first line of message.
1365 if not self.GetIssue():
1366 return 'Initial upload'
1367
1368 # When uploading subsequent patchsets, options.message is taken as the title
1369 # if options.title is not provided.
1370 if options.title:
1371 return options.title
1372 if options.message:
1373 return options.message.strip()
1374
1375 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001376 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001377 if options.force:
1378 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001379 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
1380 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001381
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001382 def CMDUpload(self, options, git_diff_args, orig_args):
1383 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001384 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001385 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001386 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001387 else:
1388 if self.GetBranch() is None:
1389 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1390
1391 # Default to diffing against common ancestor of upstream branch
1392 base_branch = self.GetCommonAncestorWithUpstream()
1393 git_diff_args = [base_branch, 'HEAD']
1394
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001395 # Fast best-effort checks to abort before running potentially expensive
1396 # hooks if uploading is likely to fail anyway. Passing these checks does
1397 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001398 self.EnsureAuthenticated(force=options.force)
1399 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001400
1401 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001402 watchlist = watchlists.Watchlists(settings.GetRoot())
1403 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001404 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001405 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001406
Edward Lemur5a644f82020-03-18 16:44:57 +00001407 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001408 if not options.bypass_hooks:
Edward Lemur2c62b332020-03-12 22:12:33 +00001409 hook_results = self.RunHook(
1410 committing=False,
1411 may_prompt=not options.force,
1412 verbose=options.verbose,
1413 parallel=options.parallel,
1414 upstream=base_branch,
Edward Lemur5a644f82020-03-18 16:44:57 +00001415 description=change_desc.description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001416 all_files=False,
1417 resultdb=options.resultdb)
Edward Lemur227d5102020-02-25 23:45:35 +00001418 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001419
Aaron Gable13101a62018-02-09 13:20:41 -08001420 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001421 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001422 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001423 if not ret:
Edward Lemur85153282020-02-14 22:06:29 +00001424 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001425 'last-upload-hash', scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001426 # Run post upload hooks, if specified.
1427 if settings.GetRunPostUploadHook():
Edward Lemur5a644f82020-03-18 16:44:57 +00001428 self.RunPostUploadHook(
1429 options.verbose, base_branch, change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001430
1431 # Upload all dependencies if specified.
1432 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001433 print()
1434 print('--dependencies has been specified.')
1435 print('All dependent local branches will be re-uploaded.')
1436 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001437 # Remove the dependencies flag from args so that we do not end up in a
1438 # loop.
1439 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001440 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001441 return ret
1442
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001443 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001444 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001445
1446 Issue must have been already uploaded and known.
1447 """
1448 assert new_state in _CQState.ALL_STATES
1449 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001450 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001451 vote_map = {
1452 _CQState.NONE: 0,
1453 _CQState.DRY_RUN: 1,
1454 _CQState.COMMIT: 2,
1455 }
1456 labels = {'Commit-Queue': vote_map[new_state]}
1457 notify = False if new_state == _CQState.DRY_RUN else None
1458 gerrit_util.SetReview(
1459 self._GetGerritHost(), self._GerritChangeIdentifier(),
1460 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001461 return 0
1462 except KeyboardInterrupt:
1463 raise
1464 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001465 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001466 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001467 ' * Your project has no CQ,\n'
1468 ' * You don\'t have permission to change the CQ state,\n'
1469 ' * There\'s a bug in this code (see stack trace below).\n'
1470 'Consider specifying which bots to trigger manually or asking your '
1471 'project owners for permissions or contacting Chrome Infra at:\n'
1472 'https://www.chromium.org/infra\n\n' %
1473 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001474 # Still raise exception so that stack trace is printed.
1475 raise
1476
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001477 def _GetGerritHost(self):
1478 # Lazy load of configs.
1479 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001480 if self._gerrit_host and '.' not in self._gerrit_host:
1481 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1482 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001483 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001484 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001485 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001486 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001487 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1488 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001489 return self._gerrit_host
1490
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001491 def _GetGitHost(self):
1492 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001493 remote_url = self.GetRemoteUrl()
1494 if not remote_url:
1495 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001496 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001497
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498 def GetCodereviewServer(self):
1499 if not self._gerrit_server:
1500 # If we're on a branch then get the server potentially associated
1501 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001502 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001503 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001504 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001505 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001506 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001507 if not self._gerrit_server:
1508 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1509 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001510 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001511 parts[0] = parts[0] + '-review'
1512 self._gerrit_host = '.'.join(parts)
1513 self._gerrit_server = 'https://%s' % self._gerrit_host
1514 return self._gerrit_server
1515
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001516 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001517 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001518 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001519 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001520 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001521 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001522 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001523 if project.endswith('.git'):
1524 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001525 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1526 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1527 # gitiles/git-over-https protocol. E.g.,
1528 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1529 # as
1530 # https://chromium.googlesource.com/v8/v8
1531 if project.startswith('a/'):
1532 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001533 return project
1534
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001535 def _GerritChangeIdentifier(self):
1536 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1537
1538 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001539 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001540 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001541 project = self._GetGerritProject()
1542 if project:
1543 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1544 # Fall back on still unique, but less efficient change number.
1545 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001546
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001547 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001549 if settings.GetGerritSkipEnsureAuthenticated():
1550 # For projects with unusual authentication schemes.
1551 # See http://crbug.com/603378.
1552 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001553
1554 # Check presence of cookies only if using cookies-based auth method.
1555 cookie_auth = gerrit_util.Authenticator.get()
1556 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001557 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001558
Florian Mayerae510e82020-01-30 21:04:48 +00001559 remote_url = self.GetRemoteUrl()
1560 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001561 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001562 return
1563 if urllib.parse.urlparse(remote_url).scheme != 'https':
Josip906bfde2020-01-31 22:38:49 +00001564 logging.warning('Ignoring branch %(branch)s with non-https remote '
1565 '%(remote)s', {
1566 'branch': self.branch,
1567 'remote': self.GetRemoteUrl()
1568 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001569 return
1570
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001571 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001572 self.GetCodereviewServer()
1573 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001574 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001575
1576 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1577 git_auth = cookie_auth.get_auth_header(git_host)
1578 if gerrit_auth and git_auth:
1579 if gerrit_auth == git_auth:
1580 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001581 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001582 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001583 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001584 ' %s\n'
1585 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001586 ' Consider running the following command:\n'
1587 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001588 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001589 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001590 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001591 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001592 cookie_auth.get_new_password_message(git_host)))
1593 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001594 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001595 return
1596 else:
1597 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001598 ([] if gerrit_auth else [self._gerrit_host]) +
1599 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001600 DieWithError('Credentials for the following hosts are required:\n'
1601 ' %s\n'
1602 'These are read from %s (or legacy %s)\n'
1603 '%s' % (
1604 '\n '.join(missing),
1605 cookie_auth.get_gitcookies_path(),
1606 cookie_auth.get_netrc_path(),
1607 cookie_auth.get_new_password_message(git_host)))
1608
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001609 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001610 if not self.GetIssue():
1611 return
1612
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001613 status = self._GetChangeDetail()['status']
1614 if status in ('MERGED', 'ABANDONED'):
1615 DieWithError('Change %s has been %s, new uploads are not allowed' %
1616 (self.GetIssueURL(),
1617 'submitted' if status == 'MERGED' else 'abandoned'))
1618
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001619 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1620 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1621 # Apparently this check is not very important? Otherwise get_auth_email
1622 # could have been added to other implementations of Authenticator.
1623 cookies_auth = gerrit_util.Authenticator.get()
1624 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001625 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001626
1627 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001628 if self.GetIssueOwner() == cookies_user:
1629 return
1630 logging.debug('change %s owner is %s, cookies user is %s',
1631 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001632 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001633 # so ask what Gerrit thinks of this user.
1634 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1635 if details['email'] == self.GetIssueOwner():
1636 return
1637 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001638 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001639 'as %s.\n'
1640 'Uploading may fail due to lack of permissions.' %
1641 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1642 confirm_or_exit(action='upload')
1643
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001645 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001646 or CQ status, assuming adherence to a common workflow.
1647
1648 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001649 * 'error' - error from review tool (including deleted issues)
1650 * 'unsent' - no reviewers added
1651 * 'waiting' - waiting for review
1652 * 'reply' - waiting for uploader to reply to review
1653 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001654 * 'dry-run' - dry-running in the CQ
1655 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001656 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001657 """
1658 if not self.GetIssue():
1659 return None
1660
1661 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001662 data = self._GetChangeDetail([
1663 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001664 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001665 return 'error'
1666
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001667 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001668 return 'closed'
1669
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001670 cq_label = data['labels'].get('Commit-Queue', {})
1671 max_cq_vote = 0
1672 for vote in cq_label.get('all', []):
1673 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1674 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001675 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001676 if max_cq_vote == 1:
1677 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001678
Aaron Gable9ab38c62017-04-06 14:36:33 -07001679 if data['labels'].get('Code-Review', {}).get('approved'):
1680 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001681
1682 if not data.get('reviewers', {}).get('REVIEWER', []):
1683 return 'unsent'
1684
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001685 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001686 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001687 while messages:
1688 m = messages.pop()
1689 if m.get('tag', '').startswith('autogenerated:cq:'):
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001690 # Ignore replies from CQ.
1691 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001692 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001693 # Most recent message was by owner.
1694 return 'waiting'
1695 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001696 # Some reply from non-owner.
1697 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001698
1699 # Somehow there are no messages even though there are reviewers.
1700 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701
1702 def GetMostRecentPatchset(self):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001703 if not self.GetIssue():
1704 return None
1705
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001706 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001707 patchset = data['revisions'][data['current_revision']]['_number']
1708 self.SetPatchset(patchset)
1709 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001710
Aaron Gable636b13f2017-07-14 10:42:48 -07001711 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001712 gerrit_util.SetReview(
1713 self._GetGerritHost(), self._GerritChangeIdentifier(),
1714 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001715
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001716 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001717 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001718 # CURRENT_REVISION is included to get the latest patchset so that
1719 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001720 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001721 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1722 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001723 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001724 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001725 robot_file_comments = gerrit_util.GetChangeRobotComments(
1726 self._GetGerritHost(), self._GerritChangeIdentifier())
1727
1728 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001729 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001730 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001731 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001732 line_comments = file_comments.setdefault(path, [])
1733 line_comments.extend(
1734 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001735
1736 # Build dictionary of file comments for easy access and sorting later.
1737 # {author+date: {path: {patchset: {line: url+message}}}}
1738 comments = collections.defaultdict(
1739 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001740 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001741 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001742 tag = comment.get('tag', '')
1743 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001744 continue
1745 key = (comment['author']['email'], comment['updated'])
1746 if comment.get('side', 'REVISION') == 'PARENT':
1747 patchset = 'Base'
1748 else:
1749 patchset = 'PS%d' % comment['patch_set']
1750 line = comment.get('line', 0)
1751 url = ('https://%s/c/%s/%s/%s#%s%s' %
1752 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1753 'b' if comment.get('side') == 'PARENT' else '',
1754 str(line) if line else ''))
1755 comments[key][path][patchset][line] = (url, comment['message'])
1756
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001757 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001758 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001759 summary = self._BuildCommentSummary(msg, comments, readable)
1760 if summary:
1761 summaries.append(summary)
1762 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001763
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001764 @staticmethod
1765 def _BuildCommentSummary(msg, comments, readable):
1766 key = (msg['author']['email'], msg['date'])
1767 # Don't bother showing autogenerated messages that don't have associated
1768 # file or line comments. this will filter out most autogenerated
1769 # messages, but will keep robot comments like those from Tricium.
1770 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1771 if is_autogenerated and not comments.get(key):
1772 return None
1773 message = msg['message']
1774 # Gerrit spits out nanoseconds.
1775 assert len(msg['date'].split('.')[-1]) == 9
1776 date = datetime.datetime.strptime(msg['date'][:-3],
1777 '%Y-%m-%d %H:%M:%S.%f')
1778 if key in comments:
1779 message += '\n'
1780 for path, patchsets in sorted(comments.get(key, {}).items()):
1781 if readable:
1782 message += '\n%s' % path
1783 for patchset, lines in sorted(patchsets.items()):
1784 for line, (url, content) in sorted(lines.items()):
1785 if line:
1786 line_str = 'Line %d' % line
1787 path_str = '%s:%d:' % (path, line)
1788 else:
1789 line_str = 'File comment'
1790 path_str = '%s:0:' % path
1791 if readable:
1792 message += '\n %s, %s: %s' % (patchset, line_str, url)
1793 message += '\n %s\n' % content
1794 else:
1795 message += '\n%s ' % path_str
1796 message += '\n%s\n' % content
1797
1798 return _CommentSummary(
1799 date=date,
1800 message=message,
1801 sender=msg['author']['email'],
1802 autogenerated=is_autogenerated,
1803 # These could be inferred from the text messages and correlated with
1804 # Code-Review label maximum, however this is not reliable.
1805 # Leaving as is until the need arises.
1806 approval=False,
1807 disapproval=False,
1808 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001809
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001811 gerrit_util.AbandonChange(
1812 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001813
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001814 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001815 gerrit_util.SubmitChange(
1816 self._GetGerritHost(), self._GerritChangeIdentifier(),
1817 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001819 def _GetChangeDetail(self, options=None):
1820 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001821 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001822 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001823
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001824 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001825 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001826 options.append('CURRENT_COMMIT')
1827
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001828 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001829 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001830 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001831
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001832 for cached_options_set, data in self._detail_cache.get(cache_key, []):
1833 # Assumption: data fetched before with extra options is suitable
1834 # for return for a smaller set of options.
1835 # For example, if we cached data for
1836 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
1837 # and request is for options=[CURRENT_REVISION],
1838 # THEN we can return prior cached data.
1839 if options_set.issubset(cached_options_set):
1840 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001841
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001842 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001843 data = gerrit_util.GetChangeDetail(
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001844 self._GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001845 except gerrit_util.GerritError as e:
1846 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001847 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001848 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001849
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001850 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07001851 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001852
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00001853 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001854 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001855 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001856 data = gerrit_util.GetChangeCommit(
1857 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001858 except gerrit_util.GerritError as e:
1859 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001860 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001861 raise
agable32978d92016-11-01 12:55:02 -07001862 return data
1863
Karen Qian40c19422019-03-13 21:28:29 +00001864 def _IsCqConfigured(self):
1865 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00001866 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00001867
Olivier Robin75ee7252018-04-13 10:02:56 +02001868 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001869 if git_common.is_dirty_git_tree('land'):
1870 return 1
Karen Qian40c19422019-03-13 21:28:29 +00001871
tandriid60367b2016-06-22 05:25:12 -07001872 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00001873 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001874 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001875 'which can test and land changes for you. '
1876 'Are you sure you wish to bypass it?\n',
1877 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001878 differs = True
tandriic4344b52016-08-29 06:04:54 -07001879 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001880 # Note: git diff outputs nothing if there is no diff.
1881 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001882 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001883 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001884 if detail['current_revision'] == last_upload:
1885 differs = False
1886 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001887 print('WARNING: Local branch contents differ from latest uploaded '
1888 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001889 if differs:
1890 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001891 confirm_or_exit(
1892 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
1893 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001894 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001895 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00001896 upstream = self.GetCommonAncestorWithUpstream()
1897 if self.GetIssue():
1898 description = self.FetchDescription()
1899 else:
Edward Lemura12175c2020-03-09 16:58:26 +00001900 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00001901 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001902 committing=True,
1903 may_prompt=not force,
1904 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00001905 parallel=parallel,
1906 upstream=upstream,
1907 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001908 all_files=False,
1909 resultdb=False)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001910
1911 self.SubmitIssue(wait_for_merge=True)
1912 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07001913 links = self._GetChangeCommit().get('web_links', [])
1914 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08001915 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001916 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07001917 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001918 return 0
1919
Edward Lemurf38bc172019-09-03 21:02:13 +00001920 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001921 assert parsed_issue_arg.valid
1922
Edward Lemur125d60a2019-09-13 18:25:41 +00001923 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001924
1925 if parsed_issue_arg.hostname:
1926 self._gerrit_host = parsed_issue_arg.hostname
1927 self._gerrit_server = 'https://%s' % self._gerrit_host
1928
tandriic2405f52016-10-10 08:13:15 -07001929 try:
1930 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08001931 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07001932 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001933
1934 if not parsed_issue_arg.patchset:
1935 # Use current revision by default.
1936 revision_info = detail['revisions'][detail['current_revision']]
1937 patchset = int(revision_info['_number'])
1938 else:
1939 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001940 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001941 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1942 break
1943 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08001944 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001945 (parsed_issue_arg.patchset, self.GetIssue()))
1946
Edward Lemur125d60a2019-09-13 18:25:41 +00001947 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08001948 if remote_url.endswith('.git'):
1949 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00001950 remote_url = remote_url.rstrip('/')
1951
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001952 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00001953 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08001954
1955 if remote_url != fetch_info['url']:
1956 DieWithError('Trying to patch a change from %s but this repo appears '
1957 'to be %s.' % (fetch_info['url'], remote_url))
1958
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001959 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07001960
Aaron Gable62619a32017-06-16 08:22:09 -07001961 if force:
1962 RunGit(['reset', '--hard', 'FETCH_HEAD'])
1963 print('Checked out commit for change %i patchset %i locally' %
1964 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07001965 elif nocommit:
1966 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
1967 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07001968 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07001969 RunGit(['cherry-pick', 'FETCH_HEAD'])
1970 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07001971 (parsed_issue_arg.issue, patchset))
1972 print('Note: this created a local commit which does not have '
1973 'the same hash as the one uploaded for review. This will make '
1974 'uploading changes based on top of this branch difficult.\n'
1975 'If you want to do that, use "git cl patch --force" instead.')
1976
Stefan Zagerd08043c2017-10-12 12:07:02 -07001977 if self.GetBranch():
1978 self.SetIssue(parsed_issue_arg.issue)
1979 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001980 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Stefan Zagerd08043c2017-10-12 12:07:02 -07001981 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
1982 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
1983 else:
1984 print('WARNING: You are in detached HEAD state.\n'
1985 'The patch has been applied to your checkout, but you will not be '
1986 'able to upload a new patch set to the gerrit issue.\n'
1987 'Try using the \'-b\' option if you would like to work on a '
1988 'branch and/or upload a new patch set.')
1989
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001990 return 0
1991
tandrii16e0b4e2016-06-07 10:34:28 -07001992 def _GerritCommitMsgHookCheck(self, offer_removal):
1993 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1994 if not os.path.exists(hook):
1995 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001996 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
1997 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07001998 data = gclient_utils.FileRead(hook)
1999 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2000 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002001 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002002 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002003 'and may interfere with it in subtle ways.\n'
2004 'We recommend you remove the commit-msg hook.')
2005 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002006 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002007 gclient_utils.rm_file_or_tree(hook)
2008 print('Gerrit commit-msg hook removed.')
2009 else:
2010 print('OK, will keep Gerrit commit-msg hook in place.')
2011
Edward Lemur1b52d872019-05-09 21:12:12 +00002012 def _CleanUpOldTraces(self):
2013 """Keep only the last |MAX_TRACES| traces."""
2014 try:
2015 traces = sorted([
2016 os.path.join(TRACES_DIR, f)
2017 for f in os.listdir(TRACES_DIR)
2018 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2019 and not f.startswith('tmp'))
2020 ])
2021 traces_to_delete = traces[:-MAX_TRACES]
2022 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002023 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002024 except OSError:
2025 print('WARNING: Failed to remove old git traces from\n'
2026 ' %s'
2027 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002028
Edward Lemur5737f022019-05-17 01:24:00 +00002029 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002030 """Zip and write the git push traces stored in traces_dir."""
2031 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002032 traces_zip = trace_name + '-traces'
2033 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002034 # Create a temporary dir to store git config and gitcookies in. It will be
2035 # compressed and stored next to the traces.
2036 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002037 git_info_zip = trace_name + '-git-info'
2038
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002039 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002040
Edward Lemur1b52d872019-05-09 21:12:12 +00002041 git_push_metadata['trace_name'] = trace_name
2042 gclient_utils.FileWrite(
2043 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2044
2045 # Keep only the first 6 characters of the git hashes on the packet
2046 # trace. This greatly decreases size after compression.
2047 packet_traces = os.path.join(traces_dir, 'trace-packet')
2048 if os.path.isfile(packet_traces):
2049 contents = gclient_utils.FileRead(packet_traces)
2050 gclient_utils.FileWrite(
2051 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2052 shutil.make_archive(traces_zip, 'zip', traces_dir)
2053
2054 # Collect and compress the git config and gitcookies.
2055 git_config = RunGit(['config', '-l'])
2056 gclient_utils.FileWrite(
2057 os.path.join(git_info_dir, 'git-config'),
2058 git_config)
2059
2060 cookie_auth = gerrit_util.Authenticator.get()
2061 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2062 gitcookies_path = cookie_auth.get_gitcookies_path()
2063 if os.path.isfile(gitcookies_path):
2064 gitcookies = gclient_utils.FileRead(gitcookies_path)
2065 gclient_utils.FileWrite(
2066 os.path.join(git_info_dir, 'gitcookies'),
2067 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2068 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2069
Edward Lemur1b52d872019-05-09 21:12:12 +00002070 gclient_utils.rmtree(git_info_dir)
2071
2072 def _RunGitPushWithTraces(
2073 self, change_desc, refspec, refspec_opts, git_push_metadata):
2074 """Run git push and collect the traces resulting from the execution."""
2075 # Create a temporary directory to store traces in. Traces will be compressed
2076 # and stored in a 'traces' dir inside depot_tools.
2077 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002078 trace_name = os.path.join(
2079 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002080
2081 env = os.environ.copy()
2082 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2083 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002084 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002085 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2086 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2087 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2088
2089 try:
2090 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002091 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002092 before_push = time_time()
2093 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002094 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002095 env=env,
2096 print_stdout=True,
2097 # Flush after every line: useful for seeing progress when running as
2098 # recipe.
2099 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002100 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002101 except subprocess2.CalledProcessError as e:
2102 push_returncode = e.returncode
2103 DieWithError('Failed to create a change. Please examine output above '
2104 'for the reason of the failure.\n'
2105 'Hint: run command below to diagnose common Git/Gerrit '
2106 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002107 ' git cl creds-check\n'
2108 '\n'
2109 'If git-cl is not working correctly, file a bug under the '
2110 'Infra>SDK component including the files below.\n'
2111 'Review the files before upload, since they might contain '
2112 'sensitive information.\n'
2113 'Set the Restrict-View-Google label so that they are not '
2114 'publicly accessible.\n'
2115 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002116 change_desc)
2117 finally:
2118 execution_time = time_time() - before_push
2119 metrics.collector.add_repeated('sub_commands', {
2120 'command': 'git push',
2121 'execution_time': execution_time,
2122 'exit_code': push_returncode,
2123 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2124 })
2125
Edward Lemur1b52d872019-05-09 21:12:12 +00002126 git_push_metadata['execution_time'] = execution_time
2127 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002128 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002129
Edward Lemur1b52d872019-05-09 21:12:12 +00002130 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002131 gclient_utils.rmtree(traces_dir)
2132
2133 return push_stdout
2134
Edward Lemura12175c2020-03-09 16:58:26 +00002135 def CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00002136 self, options, git_diff_args, custom_cl_base, change_desc):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002137 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002138 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002139 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002140
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002141 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002142 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002144 # User requested to change description
2145 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002146 change_desc.prompt()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002147 change_id = self._GetChangeDetail()['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002148 change_desc.ensure_change_id(change_id)
Aaron Gableb56ad332017-01-06 15:24:31 -08002149 else: # if not self.GetIssue()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002151 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002152 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002153 if len(change_ids) == 1:
2154 change_id = change_ids[0]
2155 else:
2156 change_id = GenerateGerritChangeId(change_desc.description)
2157 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002159 if options.preserve_tryjobs:
2160 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002161
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002162 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Edward Lemur5a644f82020-03-18 16:44:57 +00002163 parent = self._ComputeParent(
2164 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002165 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002166 with gclient_utils.temporary_file() as desc_tempfile:
2167 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2168 ref_to_push = RunGit(
2169 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002170 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002171 if not git_footers.get_footer_change_id(change_desc.description):
2172 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002173 change_desc.set_description(
Edward Lemur5a644f82020-03-18 16:44:57 +00002174 self._AddChangeIdToCommitMessage(
2175 change_desc.description, git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002176 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002177 # For no-squash mode, we assume the remote called "origin" is the one we
2178 # want. It is not worthwhile to support different workflows for
2179 # no-squash mode.
2180 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002181 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2182
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002183 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002184 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2185 ref_to_push)]).splitlines()
2186 if len(commits) > 1:
2187 print('WARNING: This will upload %d commits. Run the following command '
2188 'to see which commits will be uploaded: ' % len(commits))
2189 print('git log %s..%s' % (parent, ref_to_push))
2190 print('You can also use `git squash-branch` to squash these into a '
2191 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002192 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002194 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002195 cc = []
2196 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2197 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2198 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002199 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002200 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002201 if options.cc:
2202 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002203 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002204 if change_desc.get_cced():
2205 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002206 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2207 valid_accounts = set(reviewers + cc)
2208 # TODO(crbug/877717): relax this for all hosts.
2209 else:
2210 valid_accounts = gerrit_util.ValidAccounts(
2211 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002212 logging.info('accounts %s are recognized, %s invalid',
2213 sorted(valid_accounts),
2214 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002215
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002216 # Extra options that can be specified at push time. Doc:
2217 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002218 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002219
Aaron Gable844cf292017-06-28 11:32:59 -07002220 # By default, new changes are started in WIP mode, and subsequent patchsets
2221 # don't send email. At any time, passing --send-mail will mark the change
2222 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002223 if options.send_mail:
2224 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002225 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002226 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002227 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002228 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002229 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002230
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002231 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002232 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002233
Edward Lemur5a644f82020-03-18 16:44:57 +00002234 title = self._GetTitleForUpload(options)
Aaron Gable9b713dd2016-12-14 16:04:21 -08002235 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002236 # Punctuation and whitespace in |title| must be percent-encoded.
2237 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002238
agablec6787972016-09-09 16:13:34 -07002239 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002240 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002241
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002242 for r in sorted(reviewers):
2243 if r in valid_accounts:
2244 refspec_opts.append('r=%s' % r)
2245 reviewers.remove(r)
2246 else:
2247 # TODO(tandrii): this should probably be a hard failure.
2248 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2249 % r)
2250 for c in sorted(cc):
2251 # refspec option will be rejected if cc doesn't correspond to an
2252 # account, even though REST call to add such arbitrary cc may succeed.
2253 if c in valid_accounts:
2254 refspec_opts.append('cc=%s' % c)
2255 cc.remove(c)
2256
rmistry9eadede2016-09-19 11:22:43 -07002257 if options.topic:
2258 # Documentation on Gerrit topics is here:
2259 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002260 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002261
Edward Lemur687ca902018-12-05 02:30:30 +00002262 if options.enable_auto_submit:
2263 refspec_opts.append('l=Auto-Submit+1')
2264 if options.use_commit_queue:
2265 refspec_opts.append('l=Commit-Queue+2')
2266 elif options.cq_dry_run:
2267 refspec_opts.append('l=Commit-Queue+1')
2268
2269 if change_desc.get_reviewers(tbr_only=True):
2270 score = gerrit_util.GetCodeReviewTbrScore(
2271 self._GetGerritHost(),
2272 self._GetGerritProject())
2273 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002274
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002275 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002276 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002277 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002278 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002279 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2280
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002281 refspec_suffix = ''
2282 if refspec_opts:
2283 refspec_suffix = '%' + ','.join(refspec_opts)
2284 assert ' ' not in refspec_suffix, (
2285 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2286 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2287
Edward Lemur1b52d872019-05-09 21:12:12 +00002288 git_push_metadata = {
2289 'gerrit_host': self._GetGerritHost(),
2290 'title': title or '<untitled>',
2291 'change_id': change_id,
2292 'description': change_desc.description,
2293 }
2294 push_stdout = self._RunGitPushWithTraces(
2295 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002296
2297 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002298 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 change_numbers = [m.group(1)
2300 for m in map(regex.match, push_stdout.splitlines())
2301 if m]
2302 if len(change_numbers) != 1:
2303 DieWithError(
2304 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002305 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002306 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002307 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002308
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002309 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002310 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002311 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002312 gerrit_util.AddReviewers(
2313 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002314 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002315 reviewers, cc,
2316 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002317
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002318 return 0
2319
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002320 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2321 change_desc):
2322 """Computes parent of the generated commit to be uploaded to Gerrit.
2323
2324 Returns revision or a ref name.
2325 """
2326 if custom_cl_base:
2327 # Try to avoid creating additional unintended CLs when uploading, unless
2328 # user wants to take this risk.
2329 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2330 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2331 local_ref_of_target_remote])
2332 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002333 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002334 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2335 'If you proceed with upload, more than 1 CL may be created by '
2336 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2337 'If you are certain that specified base `%s` has already been '
2338 'uploaded to Gerrit as another CL, you may proceed.\n' %
2339 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2340 if not force:
2341 confirm_or_exit(
2342 'Do you take responsibility for cleaning up potential mess '
2343 'resulting from proceeding with upload?',
2344 action='upload')
2345 return custom_cl_base
2346
Aaron Gablef97e33d2017-03-30 15:44:27 -07002347 if remote != '.':
2348 return self.GetCommonAncestorWithUpstream()
2349
2350 # If our upstream branch is local, we base our squashed commit on its
2351 # squashed version.
2352 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2353
Aaron Gablef97e33d2017-03-30 15:44:27 -07002354 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002355 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002356
2357 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002358 # TODO(tandrii): consider checking parent change in Gerrit and using its
2359 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2360 # the tree hash of the parent branch. The upside is less likely bogus
2361 # requests to reupload parent change just because it's uploadhash is
2362 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Edward Lesmesa680c232020-03-31 18:26:44 +00002363 parent = scm.GIT.GetBranchConfig(
2364 settings.GetRoot(), upstream_branch_name, 'gerritsquashhash')
Aaron Gablef97e33d2017-03-30 15:44:27 -07002365 # Verify that the upstream branch has been uploaded too, otherwise
2366 # Gerrit will create additional CLs when uploading.
2367 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2368 RunGitSilent(['rev-parse', parent + ':'])):
2369 DieWithError(
2370 '\nUpload upstream branch %s first.\n'
2371 'It is likely that this branch has been rebased since its last '
2372 'upload, so you just need to upload it again.\n'
2373 '(If you uploaded it with --no-squash, then branch dependencies '
2374 'are not supported, and you should reupload with --squash.)'
2375 % upstream_branch_name,
2376 change_desc)
2377 return parent
2378
Edward Lemura12175c2020-03-09 16:58:26 +00002379 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002380 """Re-commits using the current message, assumes the commit hook is in
2381 place.
2382 """
Edward Lemura12175c2020-03-09 16:58:26 +00002383 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002384 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002385 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002386 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002387 return new_log_desc
2388 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002389 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002390
tandriie113dfd2016-10-11 10:20:12 -07002391 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002392 try:
2393 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002394 except GerritChangeNotExists:
2395 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002396
2397 if data['status'] in ('ABANDONED', 'MERGED'):
2398 return 'CL %s is closed' % self.GetIssue()
2399
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002400 def GetGerritChange(self, patchset=None):
2401 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002402 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002403 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002404 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002405 data = self._GetChangeDetail(['ALL_REVISIONS'])
2406
2407 assert host and issue and patchset, 'CL must be uploaded first'
2408
2409 has_patchset = any(
2410 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002411 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002412 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002413 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002414 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002415
tandrii8c5a3532016-11-04 07:52:02 -07002416 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002417 'host': host,
2418 'change': issue,
2419 'project': data['project'],
2420 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002421 }
tandriie113dfd2016-10-11 10:20:12 -07002422
tandriide281ae2016-10-12 06:02:30 -07002423 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002424 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002425
Edward Lemur707d70b2018-02-07 00:50:14 +01002426 def GetReviewers(self):
2427 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002428 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002429
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002430
tandriif9aefb72016-07-01 09:06:51 -07002431def _get_bug_line_values(default_project, bugs):
2432 """Given default_project and comma separated list of bugs, yields bug line
2433 values.
2434
2435 Each bug can be either:
2436 * a number, which is combined with default_project
2437 * string, which is left as is.
2438
2439 This function may produce more than one line, because bugdroid expects one
2440 project per line.
2441
2442 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2443 ['v8:123', 'chromium:789']
2444 """
2445 default_bugs = []
2446 others = []
2447 for bug in bugs.split(','):
2448 bug = bug.strip()
2449 if bug:
2450 try:
2451 default_bugs.append(int(bug))
2452 except ValueError:
2453 others.append(bug)
2454
2455 if default_bugs:
2456 default_bugs = ','.join(map(str, default_bugs))
2457 if default_project:
2458 yield '%s:%s' % (default_project, default_bugs)
2459 else:
2460 yield default_bugs
2461 for other in sorted(others):
2462 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2463 yield other
2464
2465
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002466class ChangeDescription(object):
2467 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002468 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002469 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002470 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002471 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002472 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002473 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2474 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002475 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002476 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002477
Dan Beamd8b04ca2019-10-10 21:23:26 +00002478 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002479 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002480 if bug:
2481 regexp = re.compile(self.BUG_LINE)
2482 prefix = settings.GetBugPrefix()
2483 if not any((regexp.match(line) for line in self._description_lines)):
2484 values = list(_get_bug_line_values(prefix, bug))
2485 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002486 if fixed:
2487 regexp = re.compile(self.FIXED_LINE)
2488 prefix = settings.GetBugPrefix()
2489 if not any((regexp.match(line) for line in self._description_lines)):
2490 values = list(_get_bug_line_values(prefix, fixed))
2491 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002492
agable@chromium.org42c20792013-09-12 17:34:49 +00002493 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002494 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002495 return '\n'.join(self._description_lines)
2496
2497 def set_description(self, desc):
2498 if isinstance(desc, basestring):
2499 lines = desc.splitlines()
2500 else:
2501 lines = [line.rstrip() for line in desc]
2502 while lines and not lines[0]:
2503 lines.pop(0)
2504 while lines and not lines[-1]:
2505 lines.pop(-1)
2506 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002507
Edward Lemur5a644f82020-03-18 16:44:57 +00002508 def ensure_change_id(self, change_id):
2509 description = self.description
2510 footer_change_ids = git_footers.get_footer_change_id(description)
2511 # Make sure that the Change-Id in the description matches the given one.
2512 if footer_change_ids != [change_id]:
2513 if footer_change_ids:
2514 # Remove any existing Change-Id footers since they don't match the
2515 # expected change_id footer.
2516 description = git_footers.remove_footer(description, 'Change-Id')
2517 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
2518 'if you want to set a new one.')
2519 # Add the expected Change-Id footer.
2520 description = git_footers.add_footer_change_id(description, change_id)
2521 self.set_description(description)
2522
Edward Lemur2c62b332020-03-12 22:12:33 +00002523 def update_reviewers(
2524 self, reviewers, tbrs, add_owners_to, affected_files, author_email):
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002525 """Rewrites the R=/TBR= line(s) as a single line each.
2526
2527 Args:
2528 reviewers (list(str)) - list of additional emails to use for reviewers.
2529 tbrs (list(str)) - list of additional emails to use for TBRs.
2530 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2531 the change that are missing OWNER coverage. If this is not None, you
2532 must also pass a value for `change`.
2533 change (Change) - The Change that should be used for OWNERS lookups.
2534 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002535 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002536 assert isinstance(tbrs, list), tbrs
2537
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002538 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Edward Lemur2c62b332020-03-12 22:12:33 +00002539 assert not add_owners_to or affected_files, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002540
2541 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002542 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002543
2544 reviewers = set(reviewers)
2545 tbrs = set(tbrs)
2546 LOOKUP = {
2547 'TBR': tbrs,
2548 'R': reviewers,
2549 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002550
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002551 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002552 regexp = re.compile(self.R_LINE)
2553 matches = [regexp.match(line) for line in self._description_lines]
2554 new_desc = [l for i, l in enumerate(self._description_lines)
2555 if not matches[i]]
2556 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002557
agable@chromium.org42c20792013-09-12 17:34:49 +00002558 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002559
2560 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002561 for match in matches:
2562 if not match:
2563 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002564 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2565
2566 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002567 if add_owners_to:
Edward Lemur2c62b332020-03-12 22:12:33 +00002568 owners_db = owners.Database(settings.GetRoot(),
Edward Lemurb7f759f2020-03-04 21:20:56 +00002569 fopen=open, os_path=os.path)
Edward Lemur2c62b332020-03-12 22:12:33 +00002570 missing_files = owners_db.files_not_covered_by(affected_files,
Robert Iannucci100aa212017-04-18 17:28:26 -07002571 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002572 LOOKUP[add_owners_to].update(
Edward Lemur2c62b332020-03-12 22:12:33 +00002573 owners_db.reviewers_for(missing_files, author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002574
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002575 # If any folks ended up in both groups, remove them from tbrs.
2576 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002577
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002578 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2579 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002580
2581 # Put the new lines in the description where the old first R= line was.
2582 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2583 if 0 <= line_loc < len(self._description_lines):
2584 if new_tbr_line:
2585 self._description_lines.insert(line_loc, new_tbr_line)
2586 if new_r_line:
2587 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002588 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002589 if new_r_line:
2590 self.append_footer(new_r_line)
2591 if new_tbr_line:
2592 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002593
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002594 def set_preserve_tryjobs(self):
2595 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2596 footers = git_footers.parse_footers(self.description)
2597 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2598 if v.lower() == 'true':
2599 return
2600 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2601
Anthony Polito8b955342019-09-24 19:01:36 +00002602 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002603 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002604 self.set_description([
2605 '# Enter a description of the change.',
2606 '# This will be displayed on the codereview site.',
2607 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002608 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002609 '--------------------',
2610 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002611 bug_regexp = re.compile(self.BUG_LINE)
2612 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002613 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002614 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2615 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002616 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002617
agable@chromium.org42c20792013-09-12 17:34:49 +00002618 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002619 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002620 if not content:
2621 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002622 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002623
Bruce Dawson2377b012018-01-11 16:46:49 -08002624 # Strip off comments and default inserted "Bug:" line.
2625 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002626 (line.startswith('#') or
2627 line.rstrip() == "Bug:" or
2628 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002629 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002630 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002631 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002632
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002633 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002634 """Adds a footer line to the description.
2635
2636 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2637 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2638 that Gerrit footers are always at the end.
2639 """
2640 parsed_footer_line = git_footers.parse_footer(line)
2641 if parsed_footer_line:
2642 # Line is a gerrit footer in the form: Footer-Key: any value.
2643 # Thus, must be appended observing Gerrit footer rules.
2644 self.set_description(
2645 git_footers.add_footer(self.description,
2646 key=parsed_footer_line[0],
2647 value=parsed_footer_line[1]))
2648 return
2649
2650 if not self._description_lines:
2651 self._description_lines.append(line)
2652 return
2653
2654 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2655 if gerrit_footers:
2656 # git_footers.split_footers ensures that there is an empty line before
2657 # actual (gerrit) footers, if any. We have to keep it that way.
2658 assert top_lines and top_lines[-1] == ''
2659 top_lines, separator = top_lines[:-1], top_lines[-1:]
2660 else:
2661 separator = [] # No need for separator if there are no gerrit_footers.
2662
2663 prev_line = top_lines[-1] if top_lines else ''
2664 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2665 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2666 top_lines.append('')
2667 top_lines.append(line)
2668 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002669
tandrii99a72f22016-08-17 14:33:24 -07002670 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002671 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002672 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002673 reviewers = [match.group(2).strip()
2674 for match in matches
2675 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002676 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002677
bradnelsond975b302016-10-23 12:20:23 -07002678 def get_cced(self):
2679 """Retrieves the list of reviewers."""
2680 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2681 cced = [match.group(2).strip() for match in matches if match]
2682 return cleanup_list(cced)
2683
Nodir Turakulov23b82142017-11-16 11:04:25 -08002684 def get_hash_tags(self):
2685 """Extracts and sanitizes a list of Gerrit hashtags."""
2686 subject = (self._description_lines or ('',))[0]
2687 subject = re.sub(
2688 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2689
2690 tags = []
2691 start = 0
2692 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2693 while True:
2694 m = bracket_exp.match(subject, start)
2695 if not m:
2696 break
2697 tags.append(self.sanitize_hash_tag(m.group(1)))
2698 start = m.end()
2699
2700 if not tags:
2701 # Try "Tag: " prefix.
2702 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
2703 if m:
2704 tags.append(self.sanitize_hash_tag(m.group(1)))
2705 return tags
2706
2707 @classmethod
2708 def sanitize_hash_tag(cls, tag):
2709 """Returns a sanitized Gerrit hash tag.
2710
2711 A sanitized hashtag can be used as a git push refspec parameter value.
2712 """
2713 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
2714
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002715
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002716def FindCodereviewSettingsFile(filename='codereview.settings'):
2717 """Finds the given file starting in the cwd and going up.
2718
2719 Only looks up to the top of the repository unless an
2720 'inherit-review-settings-ok' file exists in the root of the repository.
2721 """
2722 inherit_ok_file = 'inherit-review-settings-ok'
2723 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002724 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2726 root = '/'
2727 while True:
2728 if filename in os.listdir(cwd):
2729 if os.path.isfile(os.path.join(cwd, filename)):
2730 return open(os.path.join(cwd, filename))
2731 if cwd == root:
2732 break
2733 cwd = os.path.dirname(cwd)
2734
2735
2736def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002737 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002738 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740 def SetProperty(name, setting, unset_error_ok=False):
2741 fullname = 'rietveld.' + name
2742 if setting in keyvals:
2743 RunGit(['config', fullname, keyvals[setting]])
2744 else:
2745 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2746
tandrii48df5812016-10-17 03:55:37 -07002747 if not keyvals.get('GERRIT_HOST', False):
2748 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002749 # Only server setting is required. Other settings can be absent.
2750 # In that case, we ignore errors raised during option deletion attempt.
2751 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
2752 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2753 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002754 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002755 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
2756 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002757 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2758 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00002759 SetProperty(
2760 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002762 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002763 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002764
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002765 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00002766 RunGit(['config', 'gerrit.squash-uploads',
2767 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002768
tandrii@chromium.org28253532016-04-14 13:46:56 +00002769 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002770 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002771 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002774 # should be of the form
2775 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2776 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002777 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2778 keyvals['ORIGIN_URL_CONFIG']])
2779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002780
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002781def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002782 """Downloads a network object to a local file, like urllib.urlretrieve.
2783
2784 This is necessary because urllib is broken for SSL connections via a proxy.
2785 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002786 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00002787 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002788
2789
ukai@chromium.org712d6102013-11-27 00:52:58 +00002790def hasSheBang(fname):
2791 """Checks fname is a #! script."""
2792 with open(fname) as f:
2793 return f.read(2).startswith('#!')
2794
2795
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002796def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002797 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002798
2799 Args:
2800 force: True to update hooks. False to install hooks if not present.
2801 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00002802 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002803 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2804 if not os.access(dst, os.X_OK):
2805 if os.path.exists(dst):
2806 if not force:
2807 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002808 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002809 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002810 if not hasSheBang(dst):
2811 DieWithError('Not a script: %s\n'
2812 'You need to download from\n%s\n'
2813 'into .git/hooks/commit-msg and '
2814 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002815 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2816 except Exception:
2817 if os.path.exists(dst):
2818 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002819 DieWithError('\nFailed to download hooks.\n'
2820 'You need to download from\n%s\n'
2821 'into .git/hooks/commit-msg and '
2822 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002823
2824
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002825class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002826 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002827
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002828 _GOOGLESOURCE = 'googlesource.com'
2829
2830 def __init__(self):
2831 # Cached list of [host, identity, source], where source is either
2832 # .gitcookies or .netrc.
2833 self._all_hosts = None
2834
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002835 def ensure_configured_gitcookies(self):
2836 """Runs checks and suggests fixes to make git use .gitcookies from default
2837 path."""
2838 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
2839 configured_path = RunGitSilent(
2840 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02002841 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002842 if configured_path:
2843 self._ensure_default_gitcookies_path(configured_path, default)
2844 else:
2845 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002846
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002847 @staticmethod
2848 def _ensure_default_gitcookies_path(configured_path, default_path):
2849 assert configured_path
2850 if configured_path == default_path:
2851 print('git is already configured to use your .gitcookies from %s' %
2852 configured_path)
2853 return
2854
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002855 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002856 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
2857 (configured_path, default_path))
2858
2859 if not os.path.exists(configured_path):
2860 print('However, your configured .gitcookies file is missing.')
2861 confirm_or_exit('Reconfigure git to use default .gitcookies?',
2862 action='reconfigure')
2863 RunGit(['config', '--global', 'http.cookiefile', default_path])
2864 return
2865
2866 if os.path.exists(default_path):
2867 print('WARNING: default .gitcookies file already exists %s' %
2868 default_path)
2869 DieWithError('Please delete %s manually and re-run git cl creds-check' %
2870 default_path)
2871
2872 confirm_or_exit('Move existing .gitcookies to default location?',
2873 action='move')
2874 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002875 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002876 print('Moved and reconfigured git to use .gitcookies from %s' %
2877 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002878
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002879 @staticmethod
2880 def _configure_gitcookies_path(default_path):
2881 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
2882 if os.path.exists(netrc_path):
2883 print('You seem to be using outdated .netrc for git credentials: %s' %
2884 netrc_path)
2885 print('This tool will guide you through setting up recommended '
2886 '.gitcookies store for git credentials.\n'
2887 '\n'
2888 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
2889 ' git config --global --unset http.cookiefile\n'
2890 ' mv %s %s.backup\n\n' % (default_path, default_path))
2891 confirm_or_exit(action='setup .gitcookies')
2892 RunGit(['config', '--global', 'http.cookiefile', default_path])
2893 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002894
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002895 def get_hosts_with_creds(self, include_netrc=False):
2896 if self._all_hosts is None:
2897 a = gerrit_util.CookiesAuthenticator()
2898 self._all_hosts = [
2899 (h, u, s)
2900 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002901 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
2902 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002903 )
2904 if h.endswith(self._GOOGLESOURCE)
2905 ]
2906
2907 if include_netrc:
2908 return self._all_hosts
2909 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
2910
2911 def print_current_creds(self, include_netrc=False):
2912 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
2913 if not hosts:
2914 print('No Git/Gerrit credentials found')
2915 return
Edward Lemur79d4f992019-11-11 23:49:02 +00002916 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002917 header = [('Host', 'User', 'Which file'),
2918 ['=' * l for l in lengths]]
2919 for row in (header + hosts):
2920 print('\t'.join((('%%+%ds' % l) % s)
2921 for l, s in zip(lengths, row)))
2922
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002923 @staticmethod
2924 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08002925 """Parses identity "git-<username>.domain" into <username> and domain."""
2926 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002927 # distinguishable from sub-domains. But we do know typical domains:
2928 if identity.endswith('.chromium.org'):
2929 domain = 'chromium.org'
2930 username = identity[:-len('.chromium.org')]
2931 else:
2932 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002933 if username.startswith('git-'):
2934 username = username[len('git-'):]
2935 return username, domain
2936
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002937 def _canonical_git_googlesource_host(self, host):
2938 """Normalizes Gerrit hosts (with '-review') to Git host."""
2939 assert host.endswith(self._GOOGLESOURCE)
2940 # Prefix doesn't include '.' at the end.
2941 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
2942 if prefix.endswith('-review'):
2943 prefix = prefix[:-len('-review')]
2944 return prefix + '.' + self._GOOGLESOURCE
2945
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01002946 def _canonical_gerrit_googlesource_host(self, host):
2947 git_host = self._canonical_git_googlesource_host(host)
2948 prefix = git_host.split('.', 1)[0]
2949 return prefix + '-review.' + self._GOOGLESOURCE
2950
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002951 def _get_counterpart_host(self, host):
2952 assert host.endswith(self._GOOGLESOURCE)
2953 git = self._canonical_git_googlesource_host(host)
2954 gerrit = self._canonical_gerrit_googlesource_host(git)
2955 return git if gerrit == host else gerrit
2956
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002957 def has_generic_host(self):
2958 """Returns whether generic .googlesource.com has been configured.
2959
2960 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
2961 """
2962 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
2963 if host == '.' + self._GOOGLESOURCE:
2964 return True
2965 return False
2966
2967 def _get_git_gerrit_identity_pairs(self):
2968 """Returns map from canonic host to pair of identities (Git, Gerrit).
2969
2970 One of identities might be None, meaning not configured.
2971 """
2972 host_to_identity_pairs = {}
2973 for host, identity, _ in self.get_hosts_with_creds():
2974 canonical = self._canonical_git_googlesource_host(host)
2975 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
2976 idx = 0 if canonical == host else 1
2977 pair[idx] = identity
2978 return host_to_identity_pairs
2979
2980 def get_partially_configured_hosts(self):
2981 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002982 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002983 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002984 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002985
2986 def get_conflicting_hosts(self):
2987 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002988 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002989 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002990 if None not in (i1, i2) and i1 != i2)
2991
2992 def get_duplicated_hosts(self):
2993 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002994 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002995
2996 _EXPECTED_HOST_IDENTITY_DOMAINS = {
2997 'chromium.googlesource.com': 'chromium.org',
2998 'chrome-internal.googlesource.com': 'google.com',
2999 }
3000
3001 def get_hosts_with_wrong_identities(self):
3002 """Finds hosts which **likely** reference wrong identities.
3003
3004 Note: skips hosts which have conflicting identities for Git and Gerrit.
3005 """
3006 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003007 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003008 pair = self._get_git_gerrit_identity_pairs().get(host)
3009 if pair and pair[0] == pair[1]:
3010 _, domain = self._parse_identity(pair[0])
3011 if domain != expected:
3012 hosts.add(host)
3013 return hosts
3014
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003015 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003016 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003017 hosts = sorted(hosts)
3018 assert hosts
3019 if extra_column_func is None:
3020 extras = [''] * len(hosts)
3021 else:
3022 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003023 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3024 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003025 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003026 lines.append(tmpl % he)
3027 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003028
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003029 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003030 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003031 yield ('.googlesource.com wildcard record detected',
3032 ['Chrome Infrastructure team recommends to list full host names '
3033 'explicitly.'],
3034 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003035
3036 dups = self.get_duplicated_hosts()
3037 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003038 yield ('The following hosts were defined twice',
3039 self._format_hosts(dups),
3040 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003041
3042 partial = self.get_partially_configured_hosts()
3043 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003044 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3045 'These hosts are missing',
3046 self._format_hosts(partial, lambda host: 'but %s defined' %
3047 self._get_counterpart_host(host)),
3048 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003049
3050 conflicting = self.get_conflicting_hosts()
3051 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003052 yield ('The following Git hosts have differing credentials from their '
3053 'Gerrit counterparts',
3054 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3055 tuple(self._get_git_gerrit_identity_pairs()[host])),
3056 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003057
3058 wrong = self.get_hosts_with_wrong_identities()
3059 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003060 yield ('These hosts likely use wrong identity',
3061 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3062 (self._get_git_gerrit_identity_pairs()[host][0],
3063 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3064 wrong)
3065
3066 def find_and_report_problems(self):
3067 """Returns True if there was at least one problem, else False."""
3068 found = False
3069 bad_hosts = set()
3070 for title, sublines, hosts in self._find_problems():
3071 if not found:
3072 found = True
3073 print('\n\n.gitcookies problem report:\n')
3074 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003075 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003076 if sublines:
3077 print()
3078 print(' %s' % '\n '.join(sublines))
3079 print()
3080
3081 if bad_hosts:
3082 assert found
3083 print(' You can manually remove corresponding lines in your %s file and '
3084 'visit the following URLs with correct account to generate '
3085 'correct credential lines:\n' %
3086 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3087 print(' %s' % '\n '.join(sorted(set(
3088 gerrit_util.CookiesAuthenticator().get_new_password_url(
3089 self._canonical_git_googlesource_host(host))
3090 for host in bad_hosts
3091 ))))
3092 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003093
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003094
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003095@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003096def CMDcreds_check(parser, args):
3097 """Checks credentials and suggests changes."""
3098 _, _ = parser.parse_args(args)
3099
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003100 # Code below checks .gitcookies. Abort if using something else.
3101 authn = gerrit_util.Authenticator.get()
3102 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003103 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003104 'This command is not designed for bot environment. It checks '
3105 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003106 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3107 if isinstance(authn, gerrit_util.GceAuthenticator):
3108 message += (
3109 '\n'
3110 'If you need to run this on GCE or a cloudtop instance, '
3111 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3112 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003113
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003114 checker = _GitCookiesChecker()
3115 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003116
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003117 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003118 checker.print_current_creds(include_netrc=True)
3119
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003120 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003121 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003122 return 0
3123 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003124
3125
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003126@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003127def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003128 """Gets or sets base-url for this branch."""
Edward Lesmes50da7702020-03-30 19:23:43 +00003129 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003130 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003131 _, args = parser.parse_args(args)
3132 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003133 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003134 return RunGit(['config', 'branch.%s.base-url' % branch],
3135 error_ok=False).strip()
3136 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003137 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003138 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3139 error_ok=False).strip()
3140
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003141
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003142def color_for_status(status):
3143 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003144 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003145 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003146 'unsent': BOLD + Fore.YELLOW,
3147 'waiting': BOLD + Fore.RED,
3148 'reply': BOLD + Fore.YELLOW,
3149 'not lgtm': BOLD + Fore.RED,
3150 'lgtm': BOLD + Fore.GREEN,
3151 'commit': BOLD + Fore.MAGENTA,
3152 'closed': BOLD + Fore.CYAN,
3153 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003154 }.get(status, Fore.WHITE)
3155
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003156
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003157def get_cl_statuses(changes, fine_grained, max_processes=None):
3158 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003159
3160 If fine_grained is true, this will fetch CL statuses from the server.
3161 Otherwise, simply indicate if there's a matching url for the given branches.
3162
3163 If max_processes is specified, it is used as the maximum number of processes
3164 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3165 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003166
3167 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003168 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003169 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003170 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003171
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003172 if not fine_grained:
3173 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003174 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003175 for cl in changes:
3176 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003177 return
3178
3179 # First, sort out authentication issues.
3180 logging.debug('ensuring credentials exist')
3181 for cl in changes:
3182 cl.EnsureAuthenticated(force=False, refresh=True)
3183
3184 def fetch(cl):
3185 try:
3186 return (cl, cl.GetStatus())
3187 except:
3188 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003189 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003190 raise
3191
3192 threads_count = len(changes)
3193 if max_processes:
3194 threads_count = max(1, min(threads_count, max_processes))
3195 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3196
Edward Lemur61bf4172020-02-24 23:22:37 +00003197 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003198 fetched_cls = set()
3199 try:
3200 it = pool.imap_unordered(fetch, changes).__iter__()
3201 while True:
3202 try:
3203 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003204 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003205 break
3206 fetched_cls.add(cl)
3207 yield cl, status
3208 finally:
3209 pool.close()
3210
3211 # Add any branches that failed to fetch.
3212 for cl in set(changes) - fetched_cls:
3213 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003214
rmistry@google.com2dd99862015-06-22 12:22:18 +00003215
Jose Lopes3863fc52020-04-07 17:00:25 +00003216def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003217 """Uploads CLs of local branches that are dependents of the current branch.
3218
3219 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003220
3221 test1 -> test2.1 -> test3.1
3222 -> test3.2
3223 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003224
3225 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3226 run on the dependent branches in this order:
3227 test2.1, test3.1, test3.2, test2.2, test3.3
3228
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003229 Note: This function does not rebase your local dependent branches. Use it
3230 when you make a change to the parent branch that will not conflict
3231 with its dependent branches, and you would like their dependencies
3232 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003233 """
3234 if git_common.is_dirty_git_tree('upload-branch-deps'):
3235 return 1
3236
3237 root_branch = cl.GetBranch()
3238 if root_branch is None:
3239 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3240 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003241 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003242 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3243 'patchset dependencies without an uploaded CL.')
3244
3245 branches = RunGit(['for-each-ref',
3246 '--format=%(refname:short) %(upstream:short)',
3247 'refs/heads'])
3248 if not branches:
3249 print('No local branches found.')
3250 return 0
3251
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003252 # Create a dictionary of all local branches to the branches that are
3253 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003254 tracked_to_dependents = collections.defaultdict(list)
3255 for b in branches.splitlines():
3256 tokens = b.split()
3257 if len(tokens) == 2:
3258 branch_name, tracked = tokens
3259 tracked_to_dependents[tracked].append(branch_name)
3260
vapiera7fbd5a2016-06-16 09:17:49 -07003261 print()
3262 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003263 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003264
rmistry@google.com2dd99862015-06-22 12:22:18 +00003265 def traverse_dependents_preorder(branch, padding=''):
3266 dependents_to_process = tracked_to_dependents.get(branch, [])
3267 padding += ' '
3268 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003269 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003270 dependents.append(dependent)
3271 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003272
rmistry@google.com2dd99862015-06-22 12:22:18 +00003273 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003274 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003275
3276 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003277 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003278 return 0
3279
Jose Lopes3863fc52020-04-07 17:00:25 +00003280 if not force:
3281 confirm_or_exit('This command will checkout all dependent branches and run '
3282 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003283
rmistry@google.com2dd99862015-06-22 12:22:18 +00003284 # Record all dependents that failed to upload.
3285 failures = {}
3286 # Go through all dependents, checkout the branch and upload.
3287 try:
3288 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003289 print()
3290 print('--------------------------------------')
3291 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003292 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003293 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003294 try:
3295 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003296 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003297 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003298 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003299 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003300 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003301 finally:
3302 # Swap back to the original root branch.
3303 RunGit(['checkout', '-q', root_branch])
3304
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print()
3306 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003307 for dependent_branch in dependents:
3308 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003309 print(' %s : %s' % (dependent_branch, upload_status))
3310 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003311
3312 return 0
3313
3314
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003315def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003316 """Given a proposed tag name, returns a tag name that is guaranteed to be
3317 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3318 or 'foo-3', and so on."""
3319
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003320 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003321 for suffix_num in itertools.count(1):
3322 if suffix_num == 1:
3323 to_check = proposed_tag
3324 else:
3325 to_check = '%s-%d' % (proposed_tag, suffix_num)
3326
3327 if to_check not in existing_tags:
3328 return to_check
3329
3330
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003331@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003332def CMDarchive(parser, args):
3333 """Archives and deletes branches associated with closed changelists."""
3334 parser.add_option(
3335 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003336 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003337 parser.add_option(
3338 '-f', '--force', action='store_true',
3339 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003340 parser.add_option(
3341 '-d', '--dry-run', action='store_true',
3342 help='Skip the branch tagging and removal steps.')
3343 parser.add_option(
3344 '-t', '--notags', action='store_true',
3345 help='Do not tag archived branches. '
3346 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003347 parser.add_option(
3348 '-p',
3349 '--pattern',
3350 default='git-cl-archived-{issue}-{branch}',
3351 help='Format string for archive tags. '
3352 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003353
kmarshall3bff56b2016-06-06 18:31:47 -07003354 options, args = parser.parse_args(args)
3355 if args:
3356 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003357
3358 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3359 if not branches:
3360 return 0
3361
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003362 tags = RunGit(['for-each-ref', '--format=%(refname)',
3363 'refs/tags']).splitlines() or []
3364 tags = [t.split('/')[-1] for t in tags]
3365
vapiera7fbd5a2016-06-16 09:17:49 -07003366 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003367 changes = [Changelist(branchref=b)
3368 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003369 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3370 statuses = get_cl_statuses(changes,
3371 fine_grained=True,
3372 max_processes=options.maxjobs)
3373 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003374 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3375 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003376 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003377 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003378 proposal.sort()
3379
3380 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003381 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003382 return 0
3383
Edward Lemur85153282020-02-14 22:06:29 +00003384 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003385
vapiera7fbd5a2016-06-16 09:17:49 -07003386 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003387 if options.notags:
3388 for next_item in proposal:
3389 print(' ' + next_item[0])
3390 else:
3391 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3392 for next_item in proposal:
3393 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003394
kmarshall9249e012016-08-23 12:02:16 -07003395 # Quit now on precondition failure or if instructed by the user, either
3396 # via an interactive prompt or by command line flags.
3397 if options.dry_run:
3398 print('\nNo changes were made (dry run).\n')
3399 return 0
3400 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003401 print('You are currently on a branch \'%s\' which is associated with a '
3402 'closed codereview issue, so archive cannot proceed. Please '
3403 'checkout another branch and run this command again.' %
3404 current_branch)
3405 return 1
kmarshall9249e012016-08-23 12:02:16 -07003406 elif not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00003407 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07003408 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003409 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003410 return 1
3411
3412 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003413 if not options.notags:
3414 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003415
3416 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3417 # Clean up the tag if we failed to delete the branch.
3418 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003419
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003421
3422 return 0
3423
3424
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003425@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003426def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003427 """Show status of changelists.
3428
3429 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003430 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003431 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003432 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003433 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003434 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003435 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003436 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003437
3438 Also see 'git cl comments'.
3439 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003440 parser.add_option(
3441 '--no-branch-color',
3442 action='store_true',
3443 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003444 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003445 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003446 parser.add_option('-f', '--fast', action='store_true',
3447 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003448 parser.add_option(
3449 '-j', '--maxjobs', action='store', type=int,
3450 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003451 parser.add_option(
3452 '-i', '--issue', type=int,
3453 help='Operate on this issue instead of the current branch\'s implicit '
3454 'issue. Requires --field to be set.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003455 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003456 if args:
3457 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003458
iannuccie53c9352016-08-17 14:40:40 -07003459 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003460 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003463 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003464 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003465 if cl.GetIssue():
3466 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467 elif options.field == 'id':
3468 issueid = cl.GetIssue()
3469 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003471 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003472 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003474 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003475 elif options.field == 'status':
3476 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003477 elif options.field == 'url':
3478 url = cl.GetIssueURL()
3479 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003480 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003481 return 0
3482
3483 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3484 if not branches:
3485 print('No local branch found.')
3486 return 0
3487
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003489 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003490 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003491 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003492 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003493 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003494 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003495
Edward Lemur85153282020-02-14 22:06:29 +00003496 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00003497
3498 def FormatBranchName(branch, colorize=False):
3499 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3500 an asterisk when it is the current branch."""
3501
3502 asterisk = ""
3503 color = Fore.RESET
3504 if branch == current_branch:
3505 asterisk = "* "
3506 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00003507 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00003508
3509 if colorize:
3510 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003511 return asterisk + branch_name
3512
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003513 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003514
3515 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003516 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3517 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003518 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003519 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003520 branch_statuses[c.GetBranch()] = status
3521 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00003522 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003523 if url and (not status or status == 'error'):
3524 # The issue probably doesn't exist anymore.
3525 url += ' (broken)'
3526
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003527 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003528 # Turn off bold as well as colors.
3529 END = '\033[0m'
3530 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003531 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003532 color = ''
3533 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003534 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003535
Alan Cuttera3be9a52019-03-04 18:50:33 +00003536 branch_display = FormatBranchName(branch)
3537 padding = ' ' * (alignment - len(branch_display))
3538 if not options.no_branch_color:
3539 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003540
Alan Cuttera3be9a52019-03-04 18:50:33 +00003541 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3542 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003543
vapiera7fbd5a2016-06-16 09:17:49 -07003544 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003545 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003546 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003547 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003548 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003549 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003550 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003551 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003552 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003553 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003554 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003555 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003556 return 0
3557
3558
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003559def colorize_CMDstatus_doc():
3560 """To be called once in main() to add colors to git cl status help."""
3561 colors = [i for i in dir(Fore) if i[0].isupper()]
3562
3563 def colorize_line(line):
3564 for color in colors:
3565 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003566 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003567 indent = len(line) - len(line.lstrip(' ')) + 1
3568 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3569 return line
3570
3571 lines = CMDstatus.__doc__.splitlines()
3572 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3573
3574
phajdan.jre328cf92016-08-22 04:12:17 -07003575def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003576 if path == '-':
3577 json.dump(contents, sys.stdout)
3578 else:
3579 with open(path, 'w') as f:
3580 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003581
3582
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003583@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003584@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003586 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587
3588 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003589 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003590 parser.add_option('-r', '--reverse', action='store_true',
3591 help='Lookup the branch(es) for the specified issues. If '
3592 'no issues are specified, all branches with mapped '
3593 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003594 parser.add_option('--json',
3595 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003596 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597
dnj@chromium.org406c4402015-03-03 17:22:28 +00003598 if options.reverse:
3599 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003600 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003601 # Reverse issue lookup.
3602 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003603
3604 git_config = {}
3605 for config in RunGit(['config', '--get-regexp',
3606 r'branch\..*issue']).splitlines():
3607 name, _space, val = config.partition(' ')
3608 git_config[name] = val
3609
dnj@chromium.org406c4402015-03-03 17:22:28 +00003610 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00003611 issue = git_config.get(
3612 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00003613 if issue:
3614 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003615 if not args:
Carlos Caballero81923d62020-07-06 18:22:27 +00003616 args = sorted(issue_branch_map.keys())
phajdan.jre328cf92016-08-22 04:12:17 -07003617 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003618 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003619 try:
3620 issue_num = int(issue)
3621 except ValueError:
3622 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003623 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003624 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003625 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003626 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003627 if options.json:
3628 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003629 return 0
3630
3631 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003632 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003633 if not issue.valid:
3634 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3635 'or no argument to list it.\n'
3636 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003637 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003638 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003639 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003640 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003641 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
3642 if options.json:
3643 write_json(options.json, {
3644 'issue': cl.GetIssue(),
3645 'issue_url': cl.GetIssueURL(),
3646 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647 return 0
3648
3649
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003650@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003651def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003652 """Shows or posts review comments for any changelist."""
3653 parser.add_option('-a', '--add-comment', dest='comment',
3654 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003655 parser.add_option('-p', '--publish', action='store_true',
3656 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01003657 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00003658 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003659 parser.add_option('-m', '--machine-readable', dest='readable',
3660 action='store_false', default=True,
3661 help='output comments in a format compatible with '
3662 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00003663 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07003664 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003665 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003666
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003667 issue = None
3668 if options.issue:
3669 try:
3670 issue = int(options.issue)
3671 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003672 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003673
Edward Lemur934836a2019-09-09 20:16:54 +00003674 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003675
3676 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003677 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003678 return 0
3679
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003680 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
3681 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003682 for comment in summary:
3683 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003684 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003685 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003686 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003687 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003688 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00003689 elif comment.autogenerated:
3690 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003691 else:
3692 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003693 print('\n%s%s %s%s\n%s' % (
3694 color,
3695 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
3696 comment.sender,
3697 Fore.RESET,
3698 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
3699
smut@google.comc85ac942015-09-15 16:34:43 +00003700 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003701 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00003702 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003703 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
3704 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00003705 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003706 return 0
3707
3708
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003709@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003710@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003711def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003712 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003713 parser.add_option('-d', '--display', action='store_true',
3714 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003715 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003716 help='New description to set for this issue (- for stdin, '
3717 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003718 parser.add_option('-f', '--force', action='store_true',
3719 help='Delete any unpublished Gerrit edits for this issue '
3720 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003721
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003722 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003723
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003724 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003725 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003726 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003727 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003728 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003729
Edward Lemur934836a2019-09-09 20:16:54 +00003730 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003731 if target_issue_arg:
3732 kwargs['issue'] = target_issue_arg.issue
3733 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003734
3735 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003736 if not cl.GetIssue():
3737 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003738
Edward Lemur678a6842019-10-03 22:25:05 +00003739 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00003740 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003741
Edward Lemur6c6827c2020-02-06 21:15:18 +00003742 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003743
smut@google.com34fb6b12015-07-13 20:03:26 +00003744 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003745 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003746 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003747
3748 if options.new_description:
3749 text = options.new_description
3750 if text == '-':
3751 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003752 elif text == '+':
3753 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00003754 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003755
3756 description.set_description(text)
3757 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003758 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00003759 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003760 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003761 return 0
3762
3763
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003764@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003765def CMDlint(parser, args):
3766 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003767 parser.add_option('--filter', action='append', metavar='-x,+y',
3768 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003769 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003770
3771 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003772 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00003773 try:
3774 import cpplint
3775 import cpplint_chromium
3776 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003778 return 1
3779
3780 # Change the current working directory before calling lint so that it
3781 # shows the correct base.
3782 previous_cwd = os.getcwd()
3783 os.chdir(settings.GetRoot())
3784 try:
Edward Lemur934836a2019-09-09 20:16:54 +00003785 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00003786 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003787 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003788 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003789 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003790
Lei Zhangb8c62cf2020-07-15 20:09:37 +00003791 # Process cpplint arguments, if any.
3792 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
3793 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003794 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003795
Lei Zhang379d1ad2020-07-15 19:40:06 +00003796 include_regex = re.compile(settings.GetLintRegex())
3797 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00003798 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3799 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00003800 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003801 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00003802 continue
3803
3804 if ignore_regex.match(filename):
3805 print('Ignoring file %s' % filename)
3806 continue
3807
3808 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3809 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003810 finally:
3811 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003812 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003813 if cpplint._cpplint_state.error_count != 0:
3814 return 1
3815 return 0
3816
3817
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003818@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003820 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003821 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003822 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003823 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003824 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08003825 parser.add_option('--all', action='store_true',
3826 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04003827 parser.add_option('--parallel', action='store_true',
3828 help='Run all tests specified by input_api.RunTests in all '
3829 'PRESUBMIT files in parallel.')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00003830 parser.add_option('--resultdb', action='store_true',
3831 help='Run presubmit checks in the ResultSink environment '
3832 'and send results to the ResultDB database.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003833 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834
sbc@chromium.org71437c02015-04-09 19:29:40 +00003835 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837 return 1
3838
Edward Lemur934836a2019-09-09 20:16:54 +00003839 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840 if args:
3841 base_branch = args[0]
3842 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003843 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003844 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845
Gregory Nisbet29d5cf82020-02-27 08:16:58 +00003846 if cl.GetIssue():
3847 description = cl.FetchDescription()
Aaron Gable8076c282017-11-29 14:39:41 -08003848 else:
Edward Lemura12175c2020-03-09 16:58:26 +00003849 description = _create_description_from_log([base_branch])
Aaron Gable8076c282017-11-29 14:39:41 -08003850
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003851 cl.RunHook(
3852 committing=not options.upload,
3853 may_prompt=False,
3854 verbose=options.verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00003855 parallel=options.parallel,
3856 upstream=base_branch,
3857 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00003858 all_files=options.all,
3859 resultdb=options.resultdb)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003860 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003861
3862
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003863def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003864 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003865
3866 Works the same way as
3867 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3868 but can be called on demand on all platforms.
3869
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003870 The basic idea is to generate git hash of a state of the tree, original
3871 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003872 """
3873 lines = []
3874 tree_hash = RunGitSilent(['write-tree'])
3875 lines.append('tree %s' % tree_hash.strip())
3876 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3877 if code == 0:
3878 lines.append('parent %s' % parent.strip())
3879 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3880 lines.append('author %s' % author.strip())
3881 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3882 lines.append('committer %s' % committer.strip())
3883 lines.append('')
3884 # Note: Gerrit's commit-hook actually cleans message of some lines and
3885 # whitespace. This code is not doing this, but it clearly won't decrease
3886 # entropy.
3887 lines.append(message)
3888 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00003889 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003890 return 'I%s' % change_hash.strip()
3891
3892
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01003893def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00003894 """Computes the remote branch ref to use for the CL.
3895
3896 Args:
3897 remote (str): The git remote for the CL.
3898 remote_branch (str): The git remote branch for the CL.
3899 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00003900 """
3901 if not (remote and remote_branch):
3902 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003903
wittman@chromium.org455dc922015-01-26 20:15:50 +00003904 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003905 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00003906 # refs, which are then translated into the remote full symbolic refs
3907 # below.
3908 if '/' not in target_branch:
3909 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3910 else:
3911 prefix_replacements = (
3912 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3913 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3914 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3915 )
3916 match = None
3917 for regex, replacement in prefix_replacements:
3918 match = re.search(regex, target_branch)
3919 if match:
3920 remote_branch = target_branch.replace(match.group(0), replacement)
3921 break
3922 if not match:
3923 # This is a branch path but not one we recognize; use as-is.
3924 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003925 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3926 # Handle the refs that need to land in different refs.
3927 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003928
wittman@chromium.org455dc922015-01-26 20:15:50 +00003929 # Create the true path to the remote branch.
3930 # Does the following translation:
3931 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3932 # * refs/remotes/origin/master -> refs/heads/master
3933 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3934 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3935 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3936 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3937 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3938 'refs/heads/')
3939 elif remote_branch.startswith('refs/remotes/branch-heads'):
3940 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01003941
wittman@chromium.org455dc922015-01-26 20:15:50 +00003942 return remote_branch
3943
3944
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003945def cleanup_list(l):
3946 """Fixes a list so that comma separated items are put as individual items.
3947
3948 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3949 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3950 """
3951 items = sum((i.split(',') for i in l), [])
3952 stripped_items = (i.strip() for i in items)
3953 return sorted(filter(None, stripped_items))
3954
3955
Aaron Gable4db38df2017-11-03 14:59:07 -07003956@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003957@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003958def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003959 """Uploads the current changelist to codereview.
3960
3961 Can skip dependency patchset uploads for a branch by running:
3962 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003963 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00003964 git config --unset branch.branch_name.skip-deps-uploads
3965 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02003966
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003967 If the name of the checked out branch starts with "bug-" or "fix-" followed
3968 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02003969 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003970
3971 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003972 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003973 [git-cl] add support for hashtags
3974 Foo bar: implement foo
3975 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00003976 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003977 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3978 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003979 parser.add_option('--bypass-watchlists', action='store_true',
3980 dest='bypass_watchlists',
3981 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07003982 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003983 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003984 parser.add_option('--message', '-m', dest='message',
3985 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003986 parser.add_option('-b', '--bug',
3987 help='pre-populate the bug number(s) for this issue. '
3988 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003989 parser.add_option('--message-file', dest='message_file',
3990 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003991 parser.add_option('--title', '-t', dest='title',
3992 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003993 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003994 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003995 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003996 parser.add_option('--tbrs',
3997 action='append', default=[],
3998 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003999 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004000 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004001 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004002 parser.add_option('--hashtag', dest='hashtags',
4003 action='append', default=[],
4004 help=('Gerrit hashtag for new CL; '
4005 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004006 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004007 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004008 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004009 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004010 metavar='TARGET',
4011 help='Apply CL to remote ref TARGET. ' +
4012 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004013 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004014 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004015 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004016 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004017 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004018 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004019 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4020 const='TBR', help='add a set of OWNERS to TBR')
4021 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4022 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004023 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004024 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004025 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004026 'implies --send-mail')
4027 parser.add_option('-d', '--cq-dry-run',
4028 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004029 help='Send the patchset to do a CQ dry run right after '
4030 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004031 parser.add_option('--preserve-tryjobs', action='store_true',
4032 help='instruct the CQ to let tryjobs running even after '
4033 'new patchsets are uploaded instead of canceling '
4034 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004035 parser.add_option('--dependencies', action='store_true',
4036 help='Uploads CLs of all the local branches that depend on '
4037 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004038 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4039 help='Sends your change to the CQ after an approval. Only '
4040 'works on repos that have the Auto-Submit label '
4041 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004042 parser.add_option('--parallel', action='store_true',
4043 help='Run all tests specified by input_api.RunTests in all '
4044 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004045 parser.add_option('--no-autocc', action='store_true',
4046 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004047 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004048 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004049 parser.add_option('-R', '--retry-failed', action='store_true',
4050 help='Retry failed tryjobs from old patchset immediately '
4051 'after uploading new patchset. Cannot be used with '
4052 '--use-commit-queue or --cq-dry-run.')
4053 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4054 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004055 parser.add_option('--fixed', '-x',
4056 help='List of bugs that will be commented on and marked '
4057 'fixed (pre-populates "Fixed:" tag). Same format as '
4058 '-b option / "Bug:" tag. If fixing several issues, '
4059 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004060 parser.add_option('--edit-description', action='store_true', default=False,
4061 help='Modify description before upload. Cannot be used '
4062 'with --force. It is a noop when --no-squash is set '
4063 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004064 parser.add_option('--git-completion-helper', action="store_true",
4065 help=optparse.SUPPRESS_HELP)
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004066 parser.add_option('--resultdb', action='store_true',
4067 help='Run presubmit checks in the ResultSink environment '
4068 'and send results to the ResultDB database.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004069
rmistry@google.com2dd99862015-06-22 12:22:18 +00004070 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004071 (options, args) = parser.parse_args(args)
4072
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004073 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004074 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4075 if opt.help != optparse.SUPPRESS_HELP))
4076 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004077
sbc@chromium.org71437c02015-04-09 19:29:40 +00004078 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004079 return 1
4080
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004081 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004082 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004083 options.cc = cleanup_list(options.cc)
4084
Josipe827b0f2020-01-30 00:07:20 +00004085 if options.edit_description and options.force:
4086 parser.error('Only one of --force and --edit-description allowed')
4087
tandriib80458a2016-06-23 12:20:07 -07004088 if options.message_file:
4089 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004090 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004091 options.message = gclient_utils.FileRead(options.message_file)
4092 options.message_file = None
4093
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004094 if ([options.cq_dry_run,
4095 options.use_commit_queue,
4096 options.retry_failed].count(True) > 1):
4097 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4098 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004099
Aaron Gableedbc4132017-09-11 13:22:28 -07004100 if options.use_commit_queue:
4101 options.send_mail = True
4102
Edward Lesmes0dd54822020-03-26 18:24:25 +00004103 if options.squash is None:
4104 # Load default for user, repo, squash=true, in this order.
4105 options.squash = settings.GetSquashGerritUploads()
4106
Edward Lemur934836a2019-09-09 20:16:54 +00004107 cl = Changelist()
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004108 # Warm change details cache now to avoid RPCs later, reducing latency for
4109 # developers.
4110 if cl.GetIssue():
4111 cl._GetChangeDetail(
4112 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4113
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004114 if options.retry_failed and not cl.GetIssue():
4115 print('No previous patchsets, so --retry-failed has no effect.')
4116 options.retry_failed = False
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004117
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004118 # cl.GetMostRecentPatchset uses cached information, and can return the last
4119 # patchset before upload. Calling it here makes it clear that it's the
4120 # last patchset before upload. Note that GetMostRecentPatchset will fail
4121 # if no CL has been uploaded yet.
4122 if options.retry_failed:
4123 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004124
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004125 ret = cl.CMDUpload(options, args, orig_args)
4126
4127 if options.retry_failed:
4128 if ret != 0:
4129 print('Upload failed, so --retry-failed has no effect.')
4130 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004131 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004132 cl, options.buildbucket_host, latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004133 jobs = _filter_failed_for_retry(builds)
4134 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004135 print('No failed tryjobs, so --retry-failed has no effect.')
4136 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004137 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004138
4139 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004140
4141
Francois Dorayd42c6812017-05-30 15:10:20 -04004142@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004143@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004144def CMDsplit(parser, args):
4145 """Splits a branch into smaller branches and uploads CLs.
4146
4147 Creates a branch and uploads a CL for each group of files modified in the
4148 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00004149 comment, the string '$directory', is replaced with the directory containing
4150 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04004151 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004152 parser.add_option('-d', '--description', dest='description_file',
4153 help='A text file containing a CL description in which '
4154 '$directory will be replaced by each CL\'s directory.')
4155 parser.add_option('-c', '--comment', dest='comment_file',
4156 help='A text file containing a CL comment.')
4157 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004158 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004159 help='List the files and reviewers for each CL that would '
4160 'be created, but don\'t create branches or CLs.')
4161 parser.add_option('--cq-dry-run', action='store_true',
4162 help='If set, will do a cq dry run for each uploaded CL. '
4163 'Please be careful when doing this; more than ~10 CLs '
4164 'has the potential to overload our build '
4165 'infrastructure. Try to upload these not during high '
4166 'load times (usually 11-3 Mountain View time). Email '
4167 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004168 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4169 default=True,
4170 help='Sends your change to the CQ after an approval. Only '
4171 'works on repos that have the Auto-Submit label '
4172 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004173 options, _ = parser.parse_args(args)
4174
4175 if not options.description_file:
4176 parser.error('No --description flag specified.')
4177
4178 def WrappedCMDupload(args):
4179 return CMDupload(OptionParser(), args)
4180
Edward Lemur2c62b332020-03-12 22:12:33 +00004181 return split_cl.SplitCl(
4182 options.description_file, options.comment_file, Changelist,
4183 WrappedCMDupload, options.dry_run, options.cq_dry_run,
4184 options.enable_auto_submit, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04004185
4186
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004187@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004188@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004189def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004190 """DEPRECATED: Used to commit the current changelist via git-svn."""
4191 message = ('git-cl no longer supports committing to SVN repositories via '
4192 'git-svn. You probably want to use `git cl land` instead.')
4193 print(message)
4194 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195
4196
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004197@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004198@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004199def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004200 """Commits the current changelist via git.
4201
4202 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4203 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004204 """
4205 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4206 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004207 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004208 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004209 parser.add_option('--parallel', action='store_true',
4210 help='Run all tests specified by input_api.RunTests in all '
4211 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004212 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004213
Edward Lemur934836a2019-09-09 20:16:54 +00004214 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004215
Robert Iannucci2e73d432018-03-14 01:10:47 -07004216 if not cl.GetIssue():
4217 DieWithError('You must upload the change first to Gerrit.\n'
4218 ' If you would rather have `git cl land` upload '
4219 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004220 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004221 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222
4223
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004224@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004225@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004227 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228 parser.add_option('-b', dest='newbranch',
4229 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004230 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004231 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004232 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004233 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004234
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004235 group = optparse.OptionGroup(
4236 parser,
4237 'Options for continuing work on the current issue uploaded from a '
4238 'different clone (e.g. different machine). Must be used independently '
4239 'from the other options. No issue number should be specified, and the '
4240 'branch must have an issue number associated with it')
4241 group.add_option('--reapply', action='store_true', dest='reapply',
4242 help='Reset the branch and reapply the issue.\n'
4243 'CAUTION: This will undo any local changes in this '
4244 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004245
4246 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004247 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004248 parser.add_option_group(group)
4249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004251
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004252 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004253 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004254 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004255 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004256 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004257
Edward Lemur934836a2019-09-09 20:16:54 +00004258 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004259 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004260 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004261
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004262 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004263 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004264 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004265
4266 RunGit(['reset', '--hard', upstream])
4267 if options.pull:
4268 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004269
Edward Lemur678a6842019-10-03 22:25:05 +00004270 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4271 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004272
4273 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004274 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004275
Edward Lemurf38bc172019-09-03 21:02:13 +00004276 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004277 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004278 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004279
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004280 # We don't want uncommitted changes mixed up with the patch.
4281 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004282 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004283
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004284 if options.newbranch:
4285 if options.force:
4286 RunGit(['branch', '-D', options.newbranch],
4287 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00004288 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004289
Edward Lemur678a6842019-10-03 22:25:05 +00004290 cl = Changelist(
4291 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004292
Edward Lemur678a6842019-10-03 22:25:05 +00004293 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004294 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004295
Edward Lemurf38bc172019-09-03 21:02:13 +00004296 return cl.CMDPatchWithParsedIssue(
4297 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298
4299
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004300def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301 """Fetches the tree status and returns either 'open', 'closed',
4302 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004303 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004304 if url:
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00004305 status = str(urllib.request.urlopen(url).read().lower())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004306 if status.find('closed') != -1 or status == '0':
4307 return 'closed'
4308 elif status.find('open') != -1 or status == '1':
4309 return 'open'
4310 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311 return 'unset'
4312
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004313
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004314def GetTreeStatusReason():
4315 """Fetches the tree status from a json url and returns the message
4316 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004317 url = settings.GetTreeStatusUrl()
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00004318 json_url = urllib.parse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004319 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 status = json.loads(connection.read())
4321 connection.close()
4322 return status['message']
4323
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004324
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004325@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004326def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004327 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004328 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004329 status = GetTreeStatus()
4330 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004331 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 return 2
4333
vapiera7fbd5a2016-06-16 09:17:49 -07004334 print('The tree is %s' % status)
4335 print()
4336 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337 if status != 'open':
4338 return 1
4339 return 0
4340
4341
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004342@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004343def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004344 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4345 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004346 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004347 '-b', '--bot', action='append',
4348 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4349 'times to specify multiple builders. ex: '
4350 '"-b win_rel -b win_layout". See '
4351 'the try server waterfall for the builders name and the tests '
4352 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004353 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004354 '-B', '--bucket', default='',
4355 help=('Buildbucket bucket to send the try requests.'))
4356 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004357 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004358 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004359 'be determined by the try recipe that builder runs, which usually '
4360 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004361 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004362 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004363 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004364 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004365 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004366 '--category', default='git_cl_try', help='Specify custom build category.')
4367 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004368 '--project',
4369 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004370 'in recipe to determine to which repository or directory to '
4371 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004372 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004373 '-p', '--property', dest='properties', action='append', default=[],
4374 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004375 'key2=value2 etc. The value will be treated as '
4376 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004377 'NOTE: using this may make your tryjob not usable for CQ, '
4378 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004379 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004380 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4381 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004382 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004383 parser.add_option(
4384 '-R', '--retry-failed', action='store_true', default=False,
4385 help='Retry failed jobs from the latest set of tryjobs. '
4386 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004387 parser.add_option(
4388 '-i', '--issue', type=int,
4389 help='Operate on this issue instead of the current branch\'s implicit '
4390 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004391 options, args = parser.parse_args(args)
4392
machenbach@chromium.org45453142015-09-15 08:45:22 +00004393 # Make sure that all properties are prop=value pairs.
4394 bad_params = [x for x in options.properties if '=' not in x]
4395 if bad_params:
4396 parser.error('Got properties with missing "=": %s' % bad_params)
4397
maruel@chromium.org15192402012-09-06 12:38:29 +00004398 if args:
4399 parser.error('Unknown arguments: %s' % args)
4400
Edward Lemur934836a2019-09-09 20:16:54 +00004401 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004402 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004403 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004404
Edward Lemurf38bc172019-09-03 21:02:13 +00004405 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004406 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004407
tandriie113dfd2016-10-11 10:20:12 -07004408 error_message = cl.CannotTriggerTryJobReason()
4409 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004410 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004411
Edward Lemur45768512020-03-02 19:03:14 +00004412 if options.bot:
4413 if options.retry_failed:
4414 parser.error('--bot is not compatible with --retry-failed.')
4415 if not options.bucket:
4416 parser.error('A bucket (e.g. "chromium/try") is required.')
4417
4418 triggered = [b for b in options.bot if 'triggered' in b]
4419 if triggered:
4420 parser.error(
4421 'Cannot schedule builds on triggered bots: %s.\n'
4422 'This type of bot requires an initial job from a parent (usually a '
4423 'builder). Schedule a job on the parent instead.\n' % triggered)
4424
4425 if options.bucket.startswith('.master'):
4426 parser.error('Buildbot masters are not supported.')
4427
4428 project, bucket = _parse_bucket(options.bucket)
4429 if project is None or bucket is None:
4430 parser.error('Invalid bucket: %s.' % options.bucket)
4431 jobs = sorted((project, bucket, bot) for bot in options.bot)
4432 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004433 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004434 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004435 if options.verbose:
4436 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00004437 jobs = _filter_failed_for_retry(builds)
4438 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004439 print('There are no failed jobs in the latest set of jobs '
4440 '(patchset #%d), doing nothing.' % patchset)
4441 return 0
Edward Lemur45768512020-03-02 19:03:14 +00004442 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004443 if num_builders > 10:
4444 confirm_or_exit('There are %d builders with failed builds.'
4445 % num_builders, action='continue')
4446 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07004447 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004448 print('git cl try with no bots now defaults to CQ dry run.')
4449 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4450 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004451
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004452 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004453 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004454 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004455 except BuildbucketResponseException as ex:
4456 print('ERROR: %s' % ex)
4457 return 1
4458 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004459
4460
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004461@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004462def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004463 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004464 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004465 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004466 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004467 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004468 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004469 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004470 '--color', action='store_true', default=setup_color.IS_TTY,
4471 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004472 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004473 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4474 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004475 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004476 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004477 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004478 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00004479 parser.add_option(
4480 '-i', '--issue', type=int,
4481 help='Operate on this issue instead of the current branch\'s implicit '
4482 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004483 options, args = parser.parse_args(args)
4484 if args:
4485 parser.error('Unrecognized args: %s' % ' '.join(args))
4486
Edward Lemur934836a2019-09-09 20:16:54 +00004487 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004488 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004489 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004490
tandrii221ab252016-10-06 08:12:04 -07004491 patchset = options.patchset
4492 if not patchset:
4493 patchset = cl.GetMostRecentPatchset()
4494 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004495 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004496 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004497 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004498 cl.GetIssue())
4499
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004500 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004501 jobs = _fetch_tryjobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004502 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004503 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004504 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004505 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004506 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004507 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004508 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004509 return 0
4510
4511
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004512@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004513@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004514def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004515 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004516 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004517 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004518 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004520 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004521 if args:
4522 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004523 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004524 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004525 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004526 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004527
4528 # Clear configured merge-base, if there is one.
4529 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004530 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004531 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532 return 0
4533
4534
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004535@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004536def CMDweb(parser, args):
4537 """Opens the current CL in the web browser."""
4538 _, args = parser.parse_args(args)
4539 if args:
4540 parser.error('Unrecognized args: %s' % ' '.join(args))
4541
4542 issue_url = Changelist().GetIssueURL()
4543 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004544 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004545 return 1
4546
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004547 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004548 # allows us to hide the "Created new window in existing browser session."
4549 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004550 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004551 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004552 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004553 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004554 os.open(os.devnull, os.O_RDWR)
4555 try:
4556 webbrowser.open(issue_url)
4557 finally:
4558 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004559 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004560 return 0
4561
4562
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004563@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004564def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004565 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004566 parser.add_option('-d', '--dry-run', action='store_true',
4567 help='trigger in dry run mode')
4568 parser.add_option('-c', '--clear', action='store_true',
4569 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00004570 parser.add_option(
4571 '-i', '--issue', type=int,
4572 help='Operate on this issue instead of the current branch\'s implicit '
4573 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004574 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004575 if args:
4576 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004577 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004578 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004579
Edward Lemur934836a2019-09-09 20:16:54 +00004580 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004581 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004582 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004583 elif options.dry_run:
4584 state = _CQState.DRY_RUN
4585 else:
4586 state = _CQState.COMMIT
4587 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004588 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004589 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004590 return 0
4591
4592
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004593@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004594def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004595 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00004596 parser.add_option(
4597 '-i', '--issue', type=int,
4598 help='Operate on this issue instead of the current branch\'s implicit '
4599 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004600 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004601 if args:
4602 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004603 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004604 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004605 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004606 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004607 cl.CloseIssue()
4608 return 0
4609
4610
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004611@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004612def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004613 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004614 parser.add_option(
4615 '--stat',
4616 action='store_true',
4617 dest='stat',
4618 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004619 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004620 if args:
4621 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004622
Edward Lemur934836a2019-09-09 20:16:54 +00004623 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004624 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004625 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004626 if not issue:
4627 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004628
Aaron Gablea718c3e2017-08-28 17:47:28 -07004629 base = cl._GitGetBranchConfigValue('last-upload-hash')
4630 if not base:
4631 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4632 if not base:
4633 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4634 revision_info = detail['revisions'][detail['current_revision']]
4635 fetch_info = revision_info['fetch']['http']
4636 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4637 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004638
Aaron Gablea718c3e2017-08-28 17:47:28 -07004639 cmd = ['git', 'diff']
4640 if options.stat:
4641 cmd.append('--stat')
4642 cmd.append(base)
4643 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004644
4645 return 0
4646
4647
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004648@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004649def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004650 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004651 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004652 '--ignore-current',
4653 action='store_true',
4654 help='Ignore the CL\'s current reviewers and start from scratch.')
4655 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004656 '--ignore-self',
4657 action='store_true',
4658 help='Do not consider CL\'s author as an owners.')
4659 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004660 '--no-color',
4661 action='store_true',
4662 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07004663 parser.add_option(
4664 '--batch',
4665 action='store_true',
4666 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00004667 # TODO: Consider moving this to another command, since other
4668 # git-cl owners commands deal with owners for a given CL.
4669 parser.add_option(
4670 '--show-all',
4671 action='store_true',
4672 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004673 options, args = parser.parse_args(args)
4674
Edward Lemur934836a2019-09-09 20:16:54 +00004675 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00004676 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004677
Yang Guo6e269a02019-06-26 11:17:02 +00004678 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00004679 if len(args) == 0:
4680 print('No files specified for --show-all. Nothing to do.')
4681 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00004682 for arg in args:
4683 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemurb7f759f2020-03-04 21:20:56 +00004684 database = owners.Database(settings.GetRoot(), open, os.path)
Yang Guo6e269a02019-06-26 11:17:02 +00004685 database.load_data_needed_for([arg])
4686 print('Owners for %s:' % arg)
4687 for owner in sorted(database.all_possible_owners([arg], None)):
4688 print(' - %s' % owner)
4689 return 0
4690
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004691 if args:
4692 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004693 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004694 base_branch = args[0]
4695 else:
4696 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004697 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004698
Edward Lemur2c62b332020-03-12 22:12:33 +00004699 root = settings.GetRoot()
4700 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07004701
4702 if options.batch:
Edward Lemur2c62b332020-03-12 22:12:33 +00004703 db = owners.Database(root, open, os.path)
Dirk Prankebf980882017-09-02 15:08:00 -07004704 print('\n'.join(db.reviewers_for(affected_files, author)))
4705 return 0
4706
Edward Lemur2c62b332020-03-12 22:12:33 +00004707 owner_files = [f for f in affected_files if 'OWNERS' in os.path.basename(f)]
4708 original_owner_files = {
4709 f: scm.GIT.GetOldContents(root, f, base_branch).splitlines()
4710 for f in owner_files}
4711
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004712 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07004713 affected_files,
Edward Lemur2c62b332020-03-12 22:12:33 +00004714 root,
Edward Lemur707d70b2018-02-07 00:50:14 +01004715 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004716 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur2c62b332020-03-12 22:12:33 +00004717 fopen=open,
4718 os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02004719 disable_color=options.no_color,
Edward Lemur2c62b332020-03-12 22:12:33 +00004720 override_files=original_owner_files,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004721 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004722
4723
Aiden Bennerc08566e2018-10-03 17:52:42 +00004724def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004725 """Generates a diff command."""
4726 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00004727 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
4728
Aiden Benner6c18a1a2018-11-23 20:18:23 +00004729 if allow_prefix:
4730 # explicitly setting --src-prefix and --dst-prefix is necessary in the
4731 # case that diff.noprefix is set in the user's git config.
4732 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
4733 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00004734 diff_cmd += ['--no-prefix']
4735
4736 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004737
4738 if args:
4739 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004740 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004741 diff_cmd.append(arg)
4742 else:
4743 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004744
4745 return diff_cmd
4746
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004747
Jamie Madill5e96ad12020-01-13 16:08:35 +00004748def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
4749 """Runs clang-format-diff and sets a return value if necessary."""
4750
4751 if not clang_diff_files:
4752 return 0
4753
4754 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4755 # formatted. This is used to block during the presubmit.
4756 return_value = 0
4757
4758 # Locate the clang-format binary in the checkout
4759 try:
4760 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4761 except clang_format.NotFoundError as e:
4762 DieWithError(e)
4763
4764 if opts.full or settings.GetFormatFullByDefault():
4765 cmd = [clang_format_tool]
4766 if not opts.dry_run and not opts.diff:
4767 cmd.append('-i')
4768 if opts.dry_run:
4769 for diff_file in clang_diff_files:
4770 with open(diff_file, 'r') as myfile:
4771 code = myfile.read().replace('\r\n', '\n')
4772 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
4773 stdout = stdout.replace('\r\n', '\n')
4774 if opts.diff:
4775 sys.stdout.write(stdout)
4776 if code != stdout:
4777 return_value = 2
4778 else:
4779 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
4780 if opts.diff:
4781 sys.stdout.write(stdout)
4782 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00004783 try:
4784 script = clang_format.FindClangFormatScriptInChromiumTree(
4785 'clang-format-diff.py')
4786 except clang_format.NotFoundError as e:
4787 DieWithError(e)
4788
Edward Lesmes89624cd2020-04-06 17:51:56 +00004789 cmd = ['vpython', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00004790 if not opts.dry_run and not opts.diff:
4791 cmd.append('-i')
4792
4793 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00004794 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00004795
Edward Lesmes89624cd2020-04-06 17:51:56 +00004796 env = os.environ.copy()
4797 env['PATH'] = (
4798 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
4799 stdout = RunCommand(
4800 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00004801 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00004802 if opts.diff:
4803 sys.stdout.write(stdout)
4804 if opts.dry_run and len(stdout) > 0:
4805 return_value = 2
4806
4807 return return_value
4808
4809
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004810def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004811 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004812 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004813
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004814
enne@chromium.org555cfe42014-01-29 18:21:39 +00004815@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004816@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004817def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004818 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11004819 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004820 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004821 parser.add_option('--full', action='store_true',
4822 help='Reformat the full content of all touched files')
4823 parser.add_option('--dry-run', action='store_true',
4824 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004825 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004826 '--no-clang-format',
4827 dest='clang_format',
4828 action='store_false',
4829 default=True,
4830 help='Disables formatting of various file types using clang-format.')
4831 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004832 '--python',
4833 action='store_true',
4834 default=None,
4835 help='Enables python formatting on all python files.')
4836 parser.add_option(
4837 '--no-python',
4838 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00004839 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004840 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00004841 'If neither --python or --no-python are set, python files that have a '
4842 '.style.yapf file in an ancestor directory will be formatted. '
4843 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004844 parser.add_option(
4845 '--js',
4846 action='store_true',
4847 help='Format javascript code with clang-format. '
4848 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004849 parser.add_option('--diff', action='store_true',
4850 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07004851 parser.add_option('--presubmit', action='store_true',
4852 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004853 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004854
Garrett Beaty91a6f332020-01-06 16:57:24 +00004855 if opts.python is not None and opts.no_python:
4856 raise parser.error('Cannot set both --python and --no-python')
4857 if opts.no_python:
4858 opts.python = False
4859
Daniel Chengc55eecf2016-12-30 03:11:02 -08004860 # Normalize any remaining args against the current path, so paths relative to
4861 # the current directory are still resolved as expected.
4862 args = [os.path.join(os.getcwd(), arg) for arg in args]
4863
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004864 # git diff generates paths against the root of the repository. Change
4865 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004866 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004867 if rel_base_path:
4868 os.chdir(rel_base_path)
4869
digit@chromium.org29e47272013-05-17 17:01:46 +00004870 # Grab the merge-base commit, i.e. the upstream commit of the current
4871 # branch when it was created or the last time it was rebased. This is
4872 # to cover the case where the user may have called "git fetch origin",
4873 # moving the origin branch to a newer commit, but hasn't rebased yet.
4874 upstream_commit = None
4875 cl = Changelist()
4876 upstream_branch = cl.GetUpstreamBranch()
4877 if upstream_branch:
4878 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4879 upstream_commit = upstream_commit.strip()
4880
4881 if not upstream_commit:
4882 DieWithError('Could not find base commit for this branch. '
4883 'Are you in detached state?')
4884
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004885 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4886 diff_output = RunGit(changed_files_cmd)
4887 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004888 # Filter out files deleted by this CL
4889 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004890
Andreas Haas417d89c2020-02-06 10:24:27 +00004891 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00004892 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11004893
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004894 clang_diff_files = []
4895 if opts.clang_format:
4896 clang_diff_files = [
4897 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
4898 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004899 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004900 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004901
Edward Lesmes50da7702020-03-30 19:23:43 +00004902 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004903
Jamie Madill5e96ad12020-01-13 16:08:35 +00004904 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
4905 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004906
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004907 # Similar code to above, but using yapf on .py files rather than clang-format
4908 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004909 py_explicitly_disabled = opts.python is not None and not opts.python
4910 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00004911 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
4912 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004913
Aiden Bennerc08566e2018-10-03 17:52:42 +00004914 # Used for caching.
4915 yapf_configs = {}
4916 for f in python_diff_files:
4917 # Find the yapf style config for the current file, defaults to depot
4918 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004919 _FindYapfConfigFile(f, yapf_configs, top_dir)
4920
4921 # Turn on python formatting by default if a yapf config is specified.
4922 # This breaks in the case of this repo though since the specified
4923 # style file is also the global default.
4924 if opts.python is None:
4925 filtered_py_files = []
4926 for f in python_diff_files:
4927 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
4928 filtered_py_files.append(f)
4929 else:
4930 filtered_py_files = python_diff_files
4931
4932 # Note: yapf still seems to fix indentation of the entire file
4933 # even if line ranges are specified.
4934 # See https://github.com/google/yapf/issues/499
4935 if not opts.full and filtered_py_files:
4936 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
4937
Brian Sheedyb4307d52019-12-02 19:18:17 +00004938 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
4939 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
4940 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00004941
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004942 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00004943 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
4944 # Default to pep8 if not .style.yapf is found.
4945 if not yapf_style:
4946 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00004947
Peter Wend9399922020-06-17 17:33:49 +00004948 with open(f, 'r') as py_f:
4949 if 'python3' in py_f.readline():
4950 vpython_script = 'vpython3'
4951 else:
4952 vpython_script = 'vpython'
4953
4954 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00004955
4956 has_formattable_lines = False
4957 if not opts.full:
4958 # Only run yapf over changed line ranges.
4959 for diff_start, diff_len in py_line_diffs[f]:
4960 diff_end = diff_start + diff_len - 1
4961 # Yapf errors out if diff_end < diff_start but this
4962 # is a valid line range diff for a removal.
4963 if diff_end >= diff_start:
4964 has_formattable_lines = True
4965 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
4966 # If all line diffs were removals we have nothing to format.
4967 if not has_formattable_lines:
4968 continue
4969
4970 if opts.diff or opts.dry_run:
4971 cmd += ['--diff']
4972 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00004973 stdout = RunCommand(cmd,
4974 error_ok=True,
4975 cwd=top_dir,
4976 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00004977 if opts.diff:
4978 sys.stdout.write(stdout)
4979 elif len(stdout) > 0:
4980 return_value = 2
4981 else:
4982 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00004983 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004984
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004985 # Format GN build files. Always run on full build files for canonical form.
4986 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004987 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07004988 if opts.dry_run or opts.diff:
4989 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004990 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07004991 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00004992 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07004993 cwd=top_dir)
4994 if opts.dry_run and gn_ret == 2:
4995 return_value = 2 # Not formatted.
4996 elif opts.diff and gn_ret == 2:
4997 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004998 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07004999 elif gn_ret != 0:
5000 # For non-dry run cases (and non-2 return values for dry-run), a
5001 # nonzero error code indicates a failure, probably because the file
5002 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005003 DieWithError('gn format failed on ' + gn_diff_file +
5004 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005005
Ilya Shermane081cbe2017-08-15 17:51:04 -07005006 # Skip the metrics formatting from the global presubmit hook. These files have
5007 # a separate presubmit hook that issues an error if the files need formatting,
5008 # whereas the top-level presubmit script merely issues a warning. Formatting
5009 # these files is somewhat slow, so it's important not to duplicate the work.
5010 if not opts.presubmit:
5011 for xml_dir in GetDirtyMetricsDirs(diff_files):
5012 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005013 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
5014 cmd = ['vpython', pretty_print_tool, '--non-interactive']
Ilya Shermane081cbe2017-08-15 17:51:04 -07005015 if opts.dry_run or opts.diff:
5016 cmd.append('--diff')
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005017 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
5018 # `shell` param and instead replace `'vpython'` with
5019 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
5020 stdout = RunCommand(cmd, cwd=top_dir,
5021 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07005022 if opts.diff:
5023 sys.stdout.write(stdout)
5024 if opts.dry_run and stdout:
5025 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005026
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005027 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005028
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005029
Steven Holte2e664bf2017-04-21 13:10:47 -07005030def GetDirtyMetricsDirs(diff_files):
5031 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5032 metrics_xml_dirs = [
5033 os.path.join('tools', 'metrics', 'actions'),
5034 os.path.join('tools', 'metrics', 'histograms'),
5035 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00005036 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005037 os.path.join('tools', 'metrics', 'ukm'),
5038 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005039 for xml_dir in metrics_xml_dirs:
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005040 if any(
5041 os.path.normpath(file).startswith(xml_dir) for file in xml_diff_files):
Steven Holte2e664bf2017-04-21 13:10:47 -07005042 yield xml_dir
5043
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005044
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005045@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005046@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005047def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005048 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005049 _, args = parser.parse_args(args)
5050
5051 if len(args) != 1:
5052 parser.print_help()
5053 return 1
5054
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005055 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005056 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005057 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005058
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005059 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005060
Edward Lemur52969c92020-02-06 18:15:28 +00005061 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00005062 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00005063 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005064
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005065 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005066 for key, issue in [x.split() for x in output.splitlines()]:
5067 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00005068 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00005069
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005070 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005071 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005072 return 1
5073 if len(branches) == 1:
5074 RunGit(['checkout', branches[0]])
5075 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005076 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005077 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005078 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00005079 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005080 try:
5081 RunGit(['checkout', branches[int(which)]])
5082 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005083 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005084 return 1
5085
5086 return 0
5087
5088
maruel@chromium.org29404b52014-09-08 22:58:00 +00005089def CMDlol(parser, args):
5090 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005091 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005092 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5093 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5094 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005095 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005096 return 0
5097
5098
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005099class OptionParser(optparse.OptionParser):
5100 """Creates the option parse and add --verbose support."""
5101 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005102 optparse.OptionParser.__init__(
5103 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005104 self.add_option(
5105 '-v', '--verbose', action='count', default=0,
5106 help='Use 2 times for more debugging info')
5107
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005108 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005109 try:
5110 return self._parse_args(args)
5111 finally:
5112 # Regardless of success or failure of args parsing, we want to report
5113 # metrics, but only after logging has been initialized (if parsing
5114 # succeeded).
5115 global settings
5116 settings = Settings()
5117
5118 if not metrics.DISABLE_METRICS_COLLECTION:
5119 # GetViewVCUrl ultimately calls logging method.
5120 project_url = settings.GetViewVCUrl().strip('/+')
5121 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5122 metrics.collector.add('project_urls', [project_url])
5123
5124 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005125 # Create an optparse.Values object that will store only the actual passed
5126 # options, without the defaults.
5127 actual_options = optparse.Values()
5128 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5129 # Create an optparse.Values object with the default options.
5130 options = optparse.Values(self.get_default_values().__dict__)
5131 # Update it with the options passed by the user.
5132 options._update_careful(actual_options.__dict__)
5133 # Store the options passed by the user in an _actual_options attribute.
5134 # We store only the keys, and not the values, since the values can contain
5135 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005136 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005137
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005138 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005139 logging.basicConfig(
5140 level=levels[min(options.verbose, len(levels) - 1)],
5141 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5142 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005143
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005144 return options, args
5145
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005147def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005148 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005149 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005150 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005151 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005152
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005153 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005154 dispatcher = subcommand.CommandDispatcher(__name__)
5155 try:
5156 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005157 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005158 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005159 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005160 if e.code != 500:
5161 raise
5162 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005163 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005164 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005165 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005166
5167
5168if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005169 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5170 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005171 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005172 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005173 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005174 sys.exit(main(sys.argv[1:]))