blob: fadf1c93fdbf87084aedea52ac369f0dd247753a [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
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000037import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000040import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000041import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000042import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000043import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000044import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000045import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000046import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import owners_finder
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:
tandriia60502f2016-06-20 02:01:53 -0700771 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
Edward Lemur26964072020-02-19 19:18:51 +0000772 if self.squash_gerrit_uploads is None:
773 # 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
tandriia60502f2016-06-20 02:01:53 -0700778 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
Edward Lemur26964072020-02-19 19:18:51 +0000784 result = self._GetConfig('gerrit.override-squash-uploads').lower()
tandriia60502f2016-06-20 02:01:53 -0700785 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!
Edward Lemura12175c2020-03-09 16:58:26 +0000906 return RunGit(['log', '--pretty=format:%s%n%n%b'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000907
908
Aaron Gablea45ee112016-11-22 15:14:38 -0800909class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700910 def __init__(self, issue, url):
911 self.issue = issue
912 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800913 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700914
915 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800916 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700917 self.issue, self.url)
918
919
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100920_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +0000921 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100922 # TODO(tandrii): these two aren't known in Gerrit.
923 'approval', 'disapproval'])
924
925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000927 """Changelist works with one changelist in local branch.
928
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000929 Notes:
930 * Not safe for concurrent multi-{thread,process} use.
931 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700932 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 """
934
Edward Lemur125d60a2019-09-13 18:25:41 +0000935 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000936 """Create a new ChangeList instance.
937
Edward Lemurf38bc172019-09-03 21:02:13 +0000938 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000939 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000941 global settings
942 if not settings:
943 # Happens when git_cl.py is used as a utility library.
944 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.branchref = branchref
947 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000948 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +0000949 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 else:
951 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000953 self.lookedup_issue = False
954 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000958 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -0800959 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000960 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +0000961 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000962
Edward Lemur125d60a2019-09-13 18:25:41 +0000963 # Lazily cached values.
964 self._gerrit_host = None # e.g. chromium-review.googlesource.com
965 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
966 # Map from change number (issue) to its detail cache.
967 self._detail_cache = {}
968
969 if codereview_host is not None:
970 assert not codereview_host.startswith('https://'), codereview_host
971 self._gerrit_host = codereview_host
972 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000973
974 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700975 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700977 The return value is a string suitable for passing to git cl with the --cc
978 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000979 """
980 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000981 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -0800982 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000983 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
984 return self.cc
985
Daniel Cheng7227d212017-11-17 08:12:37 -0800986 def ExtendCC(self, more_cc):
987 """Extends the list of users to cc on this CL based on the changed files."""
988 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990 def GetBranch(self):
991 """Returns the short branch name, e.g. 'master'."""
992 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +0000993 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000994 if not branchref:
995 return None
996 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +0000997 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 return self.branch
999
1000 def GetBranchRef(self):
1001 """Returns the full branch name, e.g. 'refs/heads/master'."""
1002 self.GetBranch() # Poke the lazy loader.
1003 return self.branchref
1004
Edward Lemur85153282020-02-14 22:06:29 +00001005 def _GitGetBranchConfigValue(self, key, default=None):
1006 return scm.GIT.GetBranchConfig(
1007 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001008
Edward Lemur85153282020-02-14 22:06:29 +00001009 def _GitSetBranchConfigValue(self, key, value):
1010 action = 'set %s to %r' % (key, value)
1011 if not value:
1012 action = 'unset %s' % key
1013 assert self.GetBranch(), 'a branch is needed to ' + action
1014 return scm.GIT.SetBranchConfig(
1015 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001016
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001017 @staticmethod
1018 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001019 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 e.g. 'origin', 'refs/heads/master'
1021 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001022 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1023 settings.GetRoot(), branch)
1024 if not remote or not upstream_branch:
1025 DieWithError(
1026 'Unable to determine default branch to diff against.\n'
1027 'Either pass complete "git diff"-style arguments, like\n'
1028 ' git cl upload origin/master\n'
1029 'or verify this branch is set up to track another \n'
1030 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
1032 return remote, upstream_branch
1033
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001034 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001035 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001036 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001037 DieWithError('The upstream for the current branch (%s) does not exist '
1038 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001039 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001040 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 def GetUpstreamBranch(self):
1043 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001044 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001045 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001046 upstream_branch = upstream_branch.replace('refs/heads/',
1047 'refs/remotes/%s/' % remote)
1048 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1049 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 self.upstream_branch = upstream_branch
1051 return self.upstream_branch
1052
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001053 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001054 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 remote, branch = None, self.GetBranch()
1056 seen_branches = set()
1057 while branch not in seen_branches:
1058 seen_branches.add(branch)
1059 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001060 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 if remote != '.' or branch.startswith('refs/remotes'):
1062 break
1063 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001064 remotes = RunGit(['remote'], error_ok=True).split()
1065 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001066 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001067 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001069 logging.warn('Could not determine which remote this change is '
1070 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001071 else:
1072 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001073 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 branch = 'HEAD'
1075 if branch.startswith('refs/remotes'):
1076 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001077 elif branch.startswith('refs/branch-heads/'):
1078 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001079 else:
1080 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001081 return self._remote
1082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 def GetRemoteUrl(self):
1084 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1085
1086 Returns None if there is no remote.
1087 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001088 is_cached, value = self._cached_remote_url
1089 if is_cached:
1090 return value
1091
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001092 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001093 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001094
Edward Lemur298f2cf2019-02-22 21:40:39 +00001095 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001096 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001097 if host:
1098 self._cached_remote_url = (True, url)
1099 return url
1100
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001101 # If it cannot be parsed as an url, assume it is a local directory,
1102 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001103 logging.warning('"%s" doesn\'t appear to point to a git host. '
1104 'Interpreting it as a local directory.', url)
1105 if not os.path.isdir(url):
1106 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001107 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1108 'but it doesn\'t exist.',
1109 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001110 return None
1111
1112 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001113 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001114
Edward Lemur79d4f992019-11-11 23:49:02 +00001115 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001116 if not host:
1117 logging.error(
1118 'Remote "%(remote)s" for branch "%(branch)s" points to '
1119 '"%(cache_path)s", but it is misconfigured.\n'
1120 '"%(cache_path)s" must be a git repo and must have a remote named '
1121 '"%(remote)s" pointing to the git host.', {
1122 'remote': remote,
1123 'cache_path': cache_path,
1124 'branch': self.GetBranch()})
1125 return None
1126
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001127 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001128 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001130 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001131 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001132 if self.issue is None and not self.lookedup_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00001133 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001134 if self.issue is not None:
1135 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 return self.issue
1138
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001139 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 issue = self.GetIssue()
1142 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001143 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001144 server = self.GetCodereviewServer()
1145 if short:
1146 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1147 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
Edward Lemur6c6827c2020-02-06 21:15:18 +00001149 def FetchDescription(self, pretty=False):
1150 assert self.GetIssue(), 'issue is required to query Gerrit'
1151
Edward Lemur9aa1a962020-02-25 00:58:38 +00001152 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001153 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1154 current_rev = data['current_revision']
1155 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001156
1157 if not pretty:
1158 return self.description
1159
1160 # Set width to 72 columns + 2 space indent.
1161 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1162 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1163 lines = self.description.splitlines()
1164 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
1166 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001167 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001168 if self.patchset is None and not self.lookedup_patchset:
Edward Lesmes50da7702020-03-30 19:23:43 +00001169 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001170 if self.patchset is not None:
1171 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 return self.patchset
1174
Edward Lemur9aa1a962020-02-25 00:58:38 +00001175 def GetAuthor(self):
1176 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001179 """Set this branch's patchset. If patchset=0, clears the patchset."""
1180 assert self.GetBranch()
1181 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001182 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001183 else:
1184 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001185 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001187 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001188 """Set this branch's issue. If issue isn't given, clears the issue."""
1189 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001191 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001192 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001193 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001194 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001196 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001197 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 else:
tandrii5d48c322016-08-18 16:19:37 -07001199 # Reset all of these just to be clean.
1200 reset_suffixes = [
1201 'last-upload-hash',
Edward Lesmes50da7702020-03-30 19:23:43 +00001202 ISSUE_CONFIG_KEY,
1203 PATCHSET_CONFIG_KEY,
1204 CODEREVIEW_SERVER_CONFIG_KEY,
1205 'gerritsquashhash',
1206 ]
tandrii5d48c322016-08-18 16:19:37 -07001207 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001208 try:
1209 self._GitSetBranchConfigValue(prop, None)
1210 except subprocess2.CalledProcessError:
1211 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001212 msg = RunGit(['log', '-1', '--format=%B']).strip()
1213 if msg and git_footers.get_footer_change_id(msg):
1214 print('WARNING: The change patched into this branch has a Change-Id. '
1215 'Removing it.')
1216 RunGit(['commit', '--amend', '-m',
1217 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001218 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001219 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001220 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221
Edward Lemur2c62b332020-03-12 22:12:33 +00001222 def GetAffectedFiles(self, upstream):
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001223 try:
Edward Lemur2c62b332020-03-12 22:12:33 +00001224 return [f for _, f in scm.GIT.CaptureStatus(settings.GetRoot(), upstream)]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001225 except subprocess2.CalledProcessError:
1226 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001227 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001228 'This branch probably doesn\'t exist anymore. To reset the\n'
1229 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001230 ' git branch --set-upstream-to origin/master %s\n'
1231 'or replace origin/master with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001232 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001233
dsansomee2d6fd92016-09-08 00:10:47 -07001234 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001235 assert self.GetIssue(), 'issue is required to update description'
1236
1237 if gerrit_util.HasPendingChangeEdit(
1238 self._GetGerritHost(), self._GerritChangeIdentifier()):
1239 if not force:
1240 confirm_or_exit(
1241 'The description cannot be modified while the issue has a pending '
1242 'unpublished edit. Either publish the edit in the Gerrit web UI '
1243 'or delete it.\n\n', action='delete the unpublished edit')
1244
1245 gerrit_util.DeletePendingChangeEdit(
1246 self._GetGerritHost(), self._GerritChangeIdentifier())
1247 gerrit_util.SetCommitMessage(
1248 self._GetGerritHost(), self._GerritChangeIdentifier(),
1249 description, notify='NONE')
1250
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001251 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001252
Edward Lemur75526302020-02-27 22:31:05 +00001253 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001254 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001255 '--root', settings.GetRoot(),
1256 '--upstream', upstream,
1257 ]
1258
1259 args.extend(['--verbose'] * verbose)
1260
Edward Lemur99df04e2020-03-05 19:39:43 +00001261 author = self.GetAuthor()
1262 gerrit_url = self.GetCodereviewServer()
Edward Lemur227d5102020-02-25 23:45:35 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001265 if author:
1266 args.extend(['--author', author])
1267 if gerrit_url:
1268 args.extend(['--gerrit_url', gerrit_url])
Edward Lemur227d5102020-02-25 23:45:35 +00001269 if issue:
1270 args.extend(['--issue', str(issue)])
1271 if patchset:
1272 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001273
Edward Lemur75526302020-02-27 22:31:05 +00001274 return args
1275
1276 def RunHook(
1277 self, committing, may_prompt, verbose, parallel, upstream, description,
1278 all_files):
1279 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1280 args = self._GetCommonPresubmitArgs(verbose, upstream)
1281 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001282 if may_prompt:
1283 args.append('--may_prompt')
1284 if parallel:
1285 args.append('--parallel')
1286 if all_files:
1287 args.append('--all_files')
1288
1289 with gclient_utils.temporary_file() as description_file:
1290 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001291
1292 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001293 args.extend(['--json_output', json_output])
1294 args.extend(['--description_file', description_file])
1295
1296 start = time_time()
1297 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1298 exit_code = p.wait()
1299 metrics.collector.add_repeated('sub_commands', {
1300 'command': 'presubmit',
1301 'execution_time': time_time() - start,
1302 'exit_code': exit_code,
1303 })
1304
1305 if exit_code:
1306 sys.exit(exit_code)
1307
1308 json_results = gclient_utils.FileRead(json_output)
1309 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310
Edward Lemur75526302020-02-27 22:31:05 +00001311 def RunPostUploadHook(self, verbose, upstream, description):
1312 args = self._GetCommonPresubmitArgs(verbose, upstream)
1313 args.append('--post_upload')
1314
1315 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001316 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001317 args.extend(['--description_file', description_file])
1318 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1319 p.wait()
1320
Edward Lemur5a644f82020-03-18 16:44:57 +00001321 def _GetDescriptionForUpload(self, options, git_diff_args, files):
1322 # Get description message for upload.
1323 if self.GetIssue():
1324 description = self.FetchDescription()
1325 elif options.message:
1326 description = options.message
1327 else:
1328 description = _create_description_from_log(git_diff_args)
1329 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001330 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001331
1332 # Extract bug number from branch name.
1333 bug = options.bug
1334 fixed = options.fixed
1335 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
1336 self.GetBranch())
1337 if not bug and not fixed and match:
1338 if match.group('type') == 'bug':
1339 bug = match.group('bugnum')
1340 else:
1341 fixed = match.group('bugnum')
1342
1343 change_description = ChangeDescription(description, bug, fixed)
1344
1345 # Set the reviewer list now so that presubmit checks can access it.
1346 if options.reviewers or options.tbrs or options.add_owners_to:
1347 change_description.update_reviewers(
1348 options.reviewers, options.tbrs, options.add_owners_to, files,
1349 self.GetAuthor())
1350
1351 return change_description
1352
1353 def _GetTitleForUpload(self, options):
1354 # When not squashing, just return options.title.
1355 if not options.squash:
1356 return options.title
1357
1358 # On first upload, patchset title is always this string, while options.title
1359 # gets converted to first line of message.
1360 if not self.GetIssue():
1361 return 'Initial upload'
1362
1363 # When uploading subsequent patchsets, options.message is taken as the title
1364 # if options.title is not provided.
1365 if options.title:
1366 return options.title
1367 if options.message:
1368 return options.message.strip()
1369
1370 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001371 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001372 if options.force:
1373 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001374 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
1375 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001376
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001377 def CMDUpload(self, options, git_diff_args, orig_args):
1378 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001379 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001380 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001381 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001382 else:
1383 if self.GetBranch() is None:
1384 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1385
1386 # Default to diffing against common ancestor of upstream branch
1387 base_branch = self.GetCommonAncestorWithUpstream()
1388 git_diff_args = [base_branch, 'HEAD']
1389
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001390 # Fast best-effort checks to abort before running potentially expensive
1391 # hooks if uploading is likely to fail anyway. Passing these checks does
1392 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001393 self.EnsureAuthenticated(force=options.force)
1394 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001395
1396 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001397 watchlist = watchlists.Watchlists(settings.GetRoot())
1398 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001399 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001400 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001401
Edward Lemur5a644f82020-03-18 16:44:57 +00001402 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001403 if not options.bypass_hooks:
Edward Lemur2c62b332020-03-12 22:12:33 +00001404 hook_results = self.RunHook(
1405 committing=False,
1406 may_prompt=not options.force,
1407 verbose=options.verbose,
1408 parallel=options.parallel,
1409 upstream=base_branch,
Edward Lemur5a644f82020-03-18 16:44:57 +00001410 description=change_desc.description,
Edward Lemur2c62b332020-03-12 22:12:33 +00001411 all_files=False)
Edward Lemur227d5102020-02-25 23:45:35 +00001412 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001413
Aaron Gable13101a62018-02-09 13:20:41 -08001414 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001415 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001416 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001417 if not ret:
Edward Lemur85153282020-02-14 22:06:29 +00001418 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001419 'last-upload-hash', scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001420 # Run post upload hooks, if specified.
1421 if settings.GetRunPostUploadHook():
Edward Lemur5a644f82020-03-18 16:44:57 +00001422 self.RunPostUploadHook(
1423 options.verbose, base_branch, change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001424
1425 # Upload all dependencies if specified.
1426 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001427 print()
1428 print('--dependencies has been specified.')
1429 print('All dependent local branches will be re-uploaded.')
1430 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001431 # Remove the dependencies flag from args so that we do not end up in a
1432 # loop.
1433 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001434 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001435 return ret
1436
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001437 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001438 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001439
1440 Issue must have been already uploaded and known.
1441 """
1442 assert new_state in _CQState.ALL_STATES
1443 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001444 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001445 vote_map = {
1446 _CQState.NONE: 0,
1447 _CQState.DRY_RUN: 1,
1448 _CQState.COMMIT: 2,
1449 }
1450 labels = {'Commit-Queue': vote_map[new_state]}
1451 notify = False if new_state == _CQState.DRY_RUN else None
1452 gerrit_util.SetReview(
1453 self._GetGerritHost(), self._GerritChangeIdentifier(),
1454 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001455 return 0
1456 except KeyboardInterrupt:
1457 raise
1458 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001459 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001460 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001461 ' * Your project has no CQ,\n'
1462 ' * You don\'t have permission to change the CQ state,\n'
1463 ' * There\'s a bug in this code (see stack trace below).\n'
1464 'Consider specifying which bots to trigger manually or asking your '
1465 'project owners for permissions or contacting Chrome Infra at:\n'
1466 'https://www.chromium.org/infra\n\n' %
1467 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001468 # Still raise exception so that stack trace is printed.
1469 raise
1470
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001471 def _GetGerritHost(self):
1472 # Lazy load of configs.
1473 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001474 if self._gerrit_host and '.' not in self._gerrit_host:
1475 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1476 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001477 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001478 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001479 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001480 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001481 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1482 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001483 return self._gerrit_host
1484
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001485 def _GetGitHost(self):
1486 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001487 remote_url = self.GetRemoteUrl()
1488 if not remote_url:
1489 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001490 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001491
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492 def GetCodereviewServer(self):
1493 if not self._gerrit_server:
1494 # If we're on a branch then get the server potentially associated
1495 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001496 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001497 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001498 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001499 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001500 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001501 if not self._gerrit_server:
1502 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1503 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001504 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001505 parts[0] = parts[0] + '-review'
1506 self._gerrit_host = '.'.join(parts)
1507 self._gerrit_server = 'https://%s' % self._gerrit_host
1508 return self._gerrit_server
1509
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001510 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001511 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001512 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001513 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001514 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001515 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001516 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001517 if project.endswith('.git'):
1518 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001519 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1520 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1521 # gitiles/git-over-https protocol. E.g.,
1522 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1523 # as
1524 # https://chromium.googlesource.com/v8/v8
1525 if project.startswith('a/'):
1526 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001527 return project
1528
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001529 def _GerritChangeIdentifier(self):
1530 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1531
1532 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001533 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001534 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001535 project = self._GetGerritProject()
1536 if project:
1537 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1538 # Fall back on still unique, but less efficient change number.
1539 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001540
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001541 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001542 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001543 if settings.GetGerritSkipEnsureAuthenticated():
1544 # For projects with unusual authentication schemes.
1545 # See http://crbug.com/603378.
1546 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001547
1548 # Check presence of cookies only if using cookies-based auth method.
1549 cookie_auth = gerrit_util.Authenticator.get()
1550 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001551 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001552
Florian Mayerae510e82020-01-30 21:04:48 +00001553 remote_url = self.GetRemoteUrl()
1554 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001555 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001556 return
1557 if urllib.parse.urlparse(remote_url).scheme != 'https':
Josip906bfde2020-01-31 22:38:49 +00001558 logging.warning('Ignoring branch %(branch)s with non-https remote '
1559 '%(remote)s', {
1560 'branch': self.branch,
1561 'remote': self.GetRemoteUrl()
1562 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001563 return
1564
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001565 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001566 self.GetCodereviewServer()
1567 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001568 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001569
1570 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1571 git_auth = cookie_auth.get_auth_header(git_host)
1572 if gerrit_auth and git_auth:
1573 if gerrit_auth == git_auth:
1574 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001575 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001576 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001577 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001578 ' %s\n'
1579 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001580 ' Consider running the following command:\n'
1581 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001582 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001583 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001584 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001585 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001586 cookie_auth.get_new_password_message(git_host)))
1587 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001588 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001589 return
1590 else:
1591 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001592 ([] if gerrit_auth else [self._gerrit_host]) +
1593 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001594 DieWithError('Credentials for the following hosts are required:\n'
1595 ' %s\n'
1596 'These are read from %s (or legacy %s)\n'
1597 '%s' % (
1598 '\n '.join(missing),
1599 cookie_auth.get_gitcookies_path(),
1600 cookie_auth.get_netrc_path(),
1601 cookie_auth.get_new_password_message(git_host)))
1602
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001603 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001604 if not self.GetIssue():
1605 return
1606
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001607 status = self._GetChangeDetail()['status']
1608 if status in ('MERGED', 'ABANDONED'):
1609 DieWithError('Change %s has been %s, new uploads are not allowed' %
1610 (self.GetIssueURL(),
1611 'submitted' if status == 'MERGED' else 'abandoned'))
1612
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001613 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1614 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1615 # Apparently this check is not very important? Otherwise get_auth_email
1616 # could have been added to other implementations of Authenticator.
1617 cookies_auth = gerrit_util.Authenticator.get()
1618 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001619 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001620
1621 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001622 if self.GetIssueOwner() == cookies_user:
1623 return
1624 logging.debug('change %s owner is %s, cookies user is %s',
1625 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001626 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001627 # so ask what Gerrit thinks of this user.
1628 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1629 if details['email'] == self.GetIssueOwner():
1630 return
1631 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001632 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001633 'as %s.\n'
1634 'Uploading may fail due to lack of permissions.' %
1635 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1636 confirm_or_exit(action='upload')
1637
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001638 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001639 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001640 or CQ status, assuming adherence to a common workflow.
1641
1642 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001643 * 'error' - error from review tool (including deleted issues)
1644 * 'unsent' - no reviewers added
1645 * 'waiting' - waiting for review
1646 * 'reply' - waiting for uploader to reply to review
1647 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001648 * 'dry-run' - dry-running in the CQ
1649 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001650 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001651 """
1652 if not self.GetIssue():
1653 return None
1654
1655 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001656 data = self._GetChangeDetail([
1657 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001658 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001659 return 'error'
1660
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001661 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001662 return 'closed'
1663
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001664 cq_label = data['labels'].get('Commit-Queue', {})
1665 max_cq_vote = 0
1666 for vote in cq_label.get('all', []):
1667 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1668 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001669 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001670 if max_cq_vote == 1:
1671 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001672
Aaron Gable9ab38c62017-04-06 14:36:33 -07001673 if data['labels'].get('Code-Review', {}).get('approved'):
1674 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001675
1676 if not data.get('reviewers', {}).get('REVIEWER', []):
1677 return 'unsent'
1678
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001679 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001680 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001681 while messages:
1682 m = messages.pop()
1683 if m.get('tag', '').startswith('autogenerated:cq:'):
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001684 # Ignore replies from CQ.
1685 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001686 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001687 # Most recent message was by owner.
1688 return 'waiting'
1689 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001690 # Some reply from non-owner.
1691 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001692
1693 # Somehow there are no messages even though there are reviewers.
1694 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001695
1696 def GetMostRecentPatchset(self):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001697 if not self.GetIssue():
1698 return None
1699
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001700 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001701 patchset = data['revisions'][data['current_revision']]['_number']
1702 self.SetPatchset(patchset)
1703 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001704
Aaron Gable636b13f2017-07-14 10:42:48 -07001705 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001706 gerrit_util.SetReview(
1707 self._GetGerritHost(), self._GerritChangeIdentifier(),
1708 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001709
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001710 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001711 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001712 # CURRENT_REVISION is included to get the latest patchset so that
1713 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001714 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001715 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1716 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001717 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001718 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001719 robot_file_comments = gerrit_util.GetChangeRobotComments(
1720 self._GetGerritHost(), self._GerritChangeIdentifier())
1721
1722 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001723 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001724 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001725 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001726 line_comments = file_comments.setdefault(path, [])
1727 line_comments.extend(
1728 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001729
1730 # Build dictionary of file comments for easy access and sorting later.
1731 # {author+date: {path: {patchset: {line: url+message}}}}
1732 comments = collections.defaultdict(
1733 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001734 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001735 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001736 tag = comment.get('tag', '')
1737 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001738 continue
1739 key = (comment['author']['email'], comment['updated'])
1740 if comment.get('side', 'REVISION') == 'PARENT':
1741 patchset = 'Base'
1742 else:
1743 patchset = 'PS%d' % comment['patch_set']
1744 line = comment.get('line', 0)
1745 url = ('https://%s/c/%s/%s/%s#%s%s' %
1746 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1747 'b' if comment.get('side') == 'PARENT' else '',
1748 str(line) if line else ''))
1749 comments[key][path][patchset][line] = (url, comment['message'])
1750
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001751 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001752 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001753 summary = self._BuildCommentSummary(msg, comments, readable)
1754 if summary:
1755 summaries.append(summary)
1756 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001757
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001758 @staticmethod
1759 def _BuildCommentSummary(msg, comments, readable):
1760 key = (msg['author']['email'], msg['date'])
1761 # Don't bother showing autogenerated messages that don't have associated
1762 # file or line comments. this will filter out most autogenerated
1763 # messages, but will keep robot comments like those from Tricium.
1764 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1765 if is_autogenerated and not comments.get(key):
1766 return None
1767 message = msg['message']
1768 # Gerrit spits out nanoseconds.
1769 assert len(msg['date'].split('.')[-1]) == 9
1770 date = datetime.datetime.strptime(msg['date'][:-3],
1771 '%Y-%m-%d %H:%M:%S.%f')
1772 if key in comments:
1773 message += '\n'
1774 for path, patchsets in sorted(comments.get(key, {}).items()):
1775 if readable:
1776 message += '\n%s' % path
1777 for patchset, lines in sorted(patchsets.items()):
1778 for line, (url, content) in sorted(lines.items()):
1779 if line:
1780 line_str = 'Line %d' % line
1781 path_str = '%s:%d:' % (path, line)
1782 else:
1783 line_str = 'File comment'
1784 path_str = '%s:0:' % path
1785 if readable:
1786 message += '\n %s, %s: %s' % (patchset, line_str, url)
1787 message += '\n %s\n' % content
1788 else:
1789 message += '\n%s ' % path_str
1790 message += '\n%s\n' % content
1791
1792 return _CommentSummary(
1793 date=date,
1794 message=message,
1795 sender=msg['author']['email'],
1796 autogenerated=is_autogenerated,
1797 # These could be inferred from the text messages and correlated with
1798 # Code-Review label maximum, however this is not reliable.
1799 # Leaving as is until the need arises.
1800 approval=False,
1801 disapproval=False,
1802 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001805 gerrit_util.AbandonChange(
1806 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001808 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001809 gerrit_util.SubmitChange(
1810 self._GetGerritHost(), self._GerritChangeIdentifier(),
1811 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001813 def _GetChangeDetail(self, options=None):
1814 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001815 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001816 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001817
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001818 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001819 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001820 options.append('CURRENT_COMMIT')
1821
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001822 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001823 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001824 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001825
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001826 for cached_options_set, data in self._detail_cache.get(cache_key, []):
1827 # Assumption: data fetched before with extra options is suitable
1828 # for return for a smaller set of options.
1829 # For example, if we cached data for
1830 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
1831 # and request is for options=[CURRENT_REVISION],
1832 # THEN we can return prior cached data.
1833 if options_set.issubset(cached_options_set):
1834 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001835
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001836 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001837 data = gerrit_util.GetChangeDetail(
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001838 self._GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001839 except gerrit_util.GerritError as e:
1840 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001841 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001842 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001843
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001844 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07001845 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001846
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00001847 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001848 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001849 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001850 data = gerrit_util.GetChangeCommit(
1851 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001852 except gerrit_util.GerritError as e:
1853 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001854 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001855 raise
agable32978d92016-11-01 12:55:02 -07001856 return data
1857
Karen Qian40c19422019-03-13 21:28:29 +00001858 def _IsCqConfigured(self):
1859 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00001860 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00001861
Olivier Robin75ee7252018-04-13 10:02:56 +02001862 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001863 if git_common.is_dirty_git_tree('land'):
1864 return 1
Karen Qian40c19422019-03-13 21:28:29 +00001865
tandriid60367b2016-06-22 05:25:12 -07001866 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00001867 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001868 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001869 'which can test and land changes for you. '
1870 'Are you sure you wish to bypass it?\n',
1871 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001872 differs = True
tandriic4344b52016-08-29 06:04:54 -07001873 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001874 # Note: git diff outputs nothing if there is no diff.
1875 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001876 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001877 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001878 if detail['current_revision'] == last_upload:
1879 differs = False
1880 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001881 print('WARNING: Local branch contents differ from latest uploaded '
1882 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001883 if differs:
1884 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001885 confirm_or_exit(
1886 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
1887 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001888 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001889 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00001890 upstream = self.GetCommonAncestorWithUpstream()
1891 if self.GetIssue():
1892 description = self.FetchDescription()
1893 else:
Edward Lemura12175c2020-03-09 16:58:26 +00001894 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00001895 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001896 committing=True,
1897 may_prompt=not force,
1898 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00001899 parallel=parallel,
1900 upstream=upstream,
1901 description=description,
1902 all_files=False)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001903
1904 self.SubmitIssue(wait_for_merge=True)
1905 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07001906 links = self._GetChangeCommit().get('web_links', [])
1907 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08001908 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001909 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07001910 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001911 return 0
1912
Edward Lemurf38bc172019-09-03 21:02:13 +00001913 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001914 assert parsed_issue_arg.valid
1915
Edward Lemur125d60a2019-09-13 18:25:41 +00001916 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001917
1918 if parsed_issue_arg.hostname:
1919 self._gerrit_host = parsed_issue_arg.hostname
1920 self._gerrit_server = 'https://%s' % self._gerrit_host
1921
tandriic2405f52016-10-10 08:13:15 -07001922 try:
1923 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08001924 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07001925 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001926
1927 if not parsed_issue_arg.patchset:
1928 # Use current revision by default.
1929 revision_info = detail['revisions'][detail['current_revision']]
1930 patchset = int(revision_info['_number'])
1931 else:
1932 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001933 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001934 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1935 break
1936 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08001937 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001938 (parsed_issue_arg.patchset, self.GetIssue()))
1939
Edward Lemur125d60a2019-09-13 18:25:41 +00001940 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08001941 if remote_url.endswith('.git'):
1942 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00001943 remote_url = remote_url.rstrip('/')
1944
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001945 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00001946 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08001947
1948 if remote_url != fetch_info['url']:
1949 DieWithError('Trying to patch a change from %s but this repo appears '
1950 'to be %s.' % (fetch_info['url'], remote_url))
1951
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001952 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07001953
Aaron Gable62619a32017-06-16 08:22:09 -07001954 if force:
1955 RunGit(['reset', '--hard', 'FETCH_HEAD'])
1956 print('Checked out commit for change %i patchset %i locally' %
1957 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07001958 elif nocommit:
1959 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
1960 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07001961 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07001962 RunGit(['cherry-pick', 'FETCH_HEAD'])
1963 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07001964 (parsed_issue_arg.issue, patchset))
1965 print('Note: this created a local commit which does not have '
1966 'the same hash as the one uploaded for review. This will make '
1967 'uploading changes based on top of this branch difficult.\n'
1968 'If you want to do that, use "git cl patch --force" instead.')
1969
Stefan Zagerd08043c2017-10-12 12:07:02 -07001970 if self.GetBranch():
1971 self.SetIssue(parsed_issue_arg.issue)
1972 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001973 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Stefan Zagerd08043c2017-10-12 12:07:02 -07001974 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
1975 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
1976 else:
1977 print('WARNING: You are in detached HEAD state.\n'
1978 'The patch has been applied to your checkout, but you will not be '
1979 'able to upload a new patch set to the gerrit issue.\n'
1980 'Try using the \'-b\' option if you would like to work on a '
1981 'branch and/or upload a new patch set.')
1982
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001983 return 0
1984
tandrii16e0b4e2016-06-07 10:34:28 -07001985 def _GerritCommitMsgHookCheck(self, offer_removal):
1986 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1987 if not os.path.exists(hook):
1988 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001989 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
1990 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07001991 data = gclient_utils.FileRead(hook)
1992 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
1993 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001994 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07001995 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07001996 'and may interfere with it in subtle ways.\n'
1997 'We recommend you remove the commit-msg hook.')
1998 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001999 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002000 gclient_utils.rm_file_or_tree(hook)
2001 print('Gerrit commit-msg hook removed.')
2002 else:
2003 print('OK, will keep Gerrit commit-msg hook in place.')
2004
Edward Lemur1b52d872019-05-09 21:12:12 +00002005 def _CleanUpOldTraces(self):
2006 """Keep only the last |MAX_TRACES| traces."""
2007 try:
2008 traces = sorted([
2009 os.path.join(TRACES_DIR, f)
2010 for f in os.listdir(TRACES_DIR)
2011 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2012 and not f.startswith('tmp'))
2013 ])
2014 traces_to_delete = traces[:-MAX_TRACES]
2015 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002016 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002017 except OSError:
2018 print('WARNING: Failed to remove old git traces from\n'
2019 ' %s'
2020 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002021
Edward Lemur5737f022019-05-17 01:24:00 +00002022 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002023 """Zip and write the git push traces stored in traces_dir."""
2024 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002025 traces_zip = trace_name + '-traces'
2026 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002027 # Create a temporary dir to store git config and gitcookies in. It will be
2028 # compressed and stored next to the traces.
2029 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002030 git_info_zip = trace_name + '-git-info'
2031
Edward Lemur5737f022019-05-17 01:24:00 +00002032 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002033 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002034 git_push_metadata['now'] = git_push_metadata['now'].decode(
2035 sys.stdin.encoding)
2036
Edward Lemur1b52d872019-05-09 21:12:12 +00002037 git_push_metadata['trace_name'] = trace_name
2038 gclient_utils.FileWrite(
2039 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2040
2041 # Keep only the first 6 characters of the git hashes on the packet
2042 # trace. This greatly decreases size after compression.
2043 packet_traces = os.path.join(traces_dir, 'trace-packet')
2044 if os.path.isfile(packet_traces):
2045 contents = gclient_utils.FileRead(packet_traces)
2046 gclient_utils.FileWrite(
2047 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2048 shutil.make_archive(traces_zip, 'zip', traces_dir)
2049
2050 # Collect and compress the git config and gitcookies.
2051 git_config = RunGit(['config', '-l'])
2052 gclient_utils.FileWrite(
2053 os.path.join(git_info_dir, 'git-config'),
2054 git_config)
2055
2056 cookie_auth = gerrit_util.Authenticator.get()
2057 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2058 gitcookies_path = cookie_auth.get_gitcookies_path()
2059 if os.path.isfile(gitcookies_path):
2060 gitcookies = gclient_utils.FileRead(gitcookies_path)
2061 gclient_utils.FileWrite(
2062 os.path.join(git_info_dir, 'gitcookies'),
2063 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2064 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2065
Edward Lemur1b52d872019-05-09 21:12:12 +00002066 gclient_utils.rmtree(git_info_dir)
2067
2068 def _RunGitPushWithTraces(
2069 self, change_desc, refspec, refspec_opts, git_push_metadata):
2070 """Run git push and collect the traces resulting from the execution."""
2071 # Create a temporary directory to store traces in. Traces will be compressed
2072 # and stored in a 'traces' dir inside depot_tools.
2073 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002074 trace_name = os.path.join(
2075 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002076
2077 env = os.environ.copy()
2078 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2079 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002080 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002081 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2082 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2083 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2084
2085 try:
2086 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002087 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002088 before_push = time_time()
2089 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002090 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002091 env=env,
2092 print_stdout=True,
2093 # Flush after every line: useful for seeing progress when running as
2094 # recipe.
2095 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002096 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002097 except subprocess2.CalledProcessError as e:
2098 push_returncode = e.returncode
2099 DieWithError('Failed to create a change. Please examine output above '
2100 'for the reason of the failure.\n'
2101 'Hint: run command below to diagnose common Git/Gerrit '
2102 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002103 ' git cl creds-check\n'
2104 '\n'
2105 'If git-cl is not working correctly, file a bug under the '
2106 'Infra>SDK component including the files below.\n'
2107 'Review the files before upload, since they might contain '
2108 'sensitive information.\n'
2109 'Set the Restrict-View-Google label so that they are not '
2110 'publicly accessible.\n'
2111 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002112 change_desc)
2113 finally:
2114 execution_time = time_time() - before_push
2115 metrics.collector.add_repeated('sub_commands', {
2116 'command': 'git push',
2117 'execution_time': execution_time,
2118 'exit_code': push_returncode,
2119 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2120 })
2121
Edward Lemur1b52d872019-05-09 21:12:12 +00002122 git_push_metadata['execution_time'] = execution_time
2123 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002124 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002125
Edward Lemur1b52d872019-05-09 21:12:12 +00002126 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002127 gclient_utils.rmtree(traces_dir)
2128
2129 return push_stdout
2130
Edward Lemura12175c2020-03-09 16:58:26 +00002131 def CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00002132 self, options, git_diff_args, custom_cl_base, change_desc):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002134 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002135 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002136
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002137 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002138 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002139 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002140 # User requested to change description
2141 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002142 change_desc.prompt()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002143 change_id = self._GetChangeDetail()['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002144 change_desc.ensure_change_id(change_id)
Aaron Gableb56ad332017-01-06 15:24:31 -08002145 else: # if not self.GetIssue()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002146 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002147 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002148 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002149 if len(change_ids) == 1:
2150 change_id = change_ids[0]
2151 else:
2152 change_id = GenerateGerritChangeId(change_desc.description)
2153 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002154
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002155 if options.preserve_tryjobs:
2156 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002157
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Edward Lemur5a644f82020-03-18 16:44:57 +00002159 parent = self._ComputeParent(
2160 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002161 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002162 with gclient_utils.temporary_file() as desc_tempfile:
2163 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2164 ref_to_push = RunGit(
2165 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002166 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002167 if not git_footers.get_footer_change_id(change_desc.description):
2168 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002169 change_desc.set_description(
Edward Lemur5a644f82020-03-18 16:44:57 +00002170 self._AddChangeIdToCommitMessage(
2171 change_desc.description, git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002172 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002173 # For no-squash mode, we assume the remote called "origin" is the one we
2174 # want. It is not worthwhile to support different workflows for
2175 # no-squash mode.
2176 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002177 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2178
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002179 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002180 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2181 ref_to_push)]).splitlines()
2182 if len(commits) > 1:
2183 print('WARNING: This will upload %d commits. Run the following command '
2184 'to see which commits will be uploaded: ' % len(commits))
2185 print('git log %s..%s' % (parent, ref_to_push))
2186 print('You can also use `git squash-branch` to squash these into a '
2187 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002188 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002189
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002190 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002191 cc = []
2192 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2193 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2194 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002195 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002196 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002197 if options.cc:
2198 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002199 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002200 if change_desc.get_cced():
2201 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002202 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2203 valid_accounts = set(reviewers + cc)
2204 # TODO(crbug/877717): relax this for all hosts.
2205 else:
2206 valid_accounts = gerrit_util.ValidAccounts(
2207 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002208 logging.info('accounts %s are recognized, %s invalid',
2209 sorted(valid_accounts),
2210 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002211
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002212 # Extra options that can be specified at push time. Doc:
2213 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002214 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002215
Aaron Gable844cf292017-06-28 11:32:59 -07002216 # By default, new changes are started in WIP mode, and subsequent patchsets
2217 # don't send email. At any time, passing --send-mail will mark the change
2218 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002219 if options.send_mail:
2220 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002221 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002222 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002223 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002224 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002225 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002226
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002227 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002228 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002229
Edward Lemur5a644f82020-03-18 16:44:57 +00002230 title = self._GetTitleForUpload(options)
Aaron Gable9b713dd2016-12-14 16:04:21 -08002231 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002232 # Punctuation and whitespace in |title| must be percent-encoded.
2233 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002234
agablec6787972016-09-09 16:13:34 -07002235 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002236 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002237
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002238 for r in sorted(reviewers):
2239 if r in valid_accounts:
2240 refspec_opts.append('r=%s' % r)
2241 reviewers.remove(r)
2242 else:
2243 # TODO(tandrii): this should probably be a hard failure.
2244 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2245 % r)
2246 for c in sorted(cc):
2247 # refspec option will be rejected if cc doesn't correspond to an
2248 # account, even though REST call to add such arbitrary cc may succeed.
2249 if c in valid_accounts:
2250 refspec_opts.append('cc=%s' % c)
2251 cc.remove(c)
2252
rmistry9eadede2016-09-19 11:22:43 -07002253 if options.topic:
2254 # Documentation on Gerrit topics is here:
2255 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002256 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002257
Edward Lemur687ca902018-12-05 02:30:30 +00002258 if options.enable_auto_submit:
2259 refspec_opts.append('l=Auto-Submit+1')
2260 if options.use_commit_queue:
2261 refspec_opts.append('l=Commit-Queue+2')
2262 elif options.cq_dry_run:
2263 refspec_opts.append('l=Commit-Queue+1')
2264
2265 if change_desc.get_reviewers(tbr_only=True):
2266 score = gerrit_util.GetCodeReviewTbrScore(
2267 self._GetGerritHost(),
2268 self._GetGerritProject())
2269 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002270
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002271 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002272 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002273 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002274 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002275 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2276
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002277 refspec_suffix = ''
2278 if refspec_opts:
2279 refspec_suffix = '%' + ','.join(refspec_opts)
2280 assert ' ' not in refspec_suffix, (
2281 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2282 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2283
Edward Lemur1b52d872019-05-09 21:12:12 +00002284 git_push_metadata = {
2285 'gerrit_host': self._GetGerritHost(),
2286 'title': title or '<untitled>',
2287 'change_id': change_id,
2288 'description': change_desc.description,
2289 }
2290 push_stdout = self._RunGitPushWithTraces(
2291 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002292
2293 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002294 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 change_numbers = [m.group(1)
2296 for m in map(regex.match, push_stdout.splitlines())
2297 if m]
2298 if len(change_numbers) != 1:
2299 DieWithError(
2300 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002301 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002303 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002304
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002305 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002306 # GetIssue() is not set in case of non-squash uploads according to tests.
2307 # TODO(agable): non-squash uploads in git cl should be removed.
2308 gerrit_util.AddReviewers(
2309 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002310 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002311 reviewers, cc,
2312 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002313
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002314 return 0
2315
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002316 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2317 change_desc):
2318 """Computes parent of the generated commit to be uploaded to Gerrit.
2319
2320 Returns revision or a ref name.
2321 """
2322 if custom_cl_base:
2323 # Try to avoid creating additional unintended CLs when uploading, unless
2324 # user wants to take this risk.
2325 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2326 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2327 local_ref_of_target_remote])
2328 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002329 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002330 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2331 'If you proceed with upload, more than 1 CL may be created by '
2332 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2333 'If you are certain that specified base `%s` has already been '
2334 'uploaded to Gerrit as another CL, you may proceed.\n' %
2335 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2336 if not force:
2337 confirm_or_exit(
2338 'Do you take responsibility for cleaning up potential mess '
2339 'resulting from proceeding with upload?',
2340 action='upload')
2341 return custom_cl_base
2342
Aaron Gablef97e33d2017-03-30 15:44:27 -07002343 if remote != '.':
2344 return self.GetCommonAncestorWithUpstream()
2345
2346 # If our upstream branch is local, we base our squashed commit on its
2347 # squashed version.
2348 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2349
Aaron Gablef97e33d2017-03-30 15:44:27 -07002350 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002351 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002352
2353 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002354 # TODO(tandrii): consider checking parent change in Gerrit and using its
2355 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2356 # the tree hash of the parent branch. The upside is less likely bogus
2357 # requests to reupload parent change just because it's uploadhash is
2358 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Edward Lesmesa680c232020-03-31 18:26:44 +00002359 parent = scm.GIT.GetBranchConfig(
2360 settings.GetRoot(), upstream_branch_name, 'gerritsquashhash')
Aaron Gablef97e33d2017-03-30 15:44:27 -07002361 # Verify that the upstream branch has been uploaded too, otherwise
2362 # Gerrit will create additional CLs when uploading.
2363 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2364 RunGitSilent(['rev-parse', parent + ':'])):
2365 DieWithError(
2366 '\nUpload upstream branch %s first.\n'
2367 'It is likely that this branch has been rebased since its last '
2368 'upload, so you just need to upload it again.\n'
2369 '(If you uploaded it with --no-squash, then branch dependencies '
2370 'are not supported, and you should reupload with --squash.)'
2371 % upstream_branch_name,
2372 change_desc)
2373 return parent
2374
Edward Lemura12175c2020-03-09 16:58:26 +00002375 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002376 """Re-commits using the current message, assumes the commit hook is in
2377 place.
2378 """
Edward Lemura12175c2020-03-09 16:58:26 +00002379 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002380 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002381 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002382 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002383 return new_log_desc
2384 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002385 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002386
tandriie113dfd2016-10-11 10:20:12 -07002387 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002388 try:
2389 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002390 except GerritChangeNotExists:
2391 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002392
2393 if data['status'] in ('ABANDONED', 'MERGED'):
2394 return 'CL %s is closed' % self.GetIssue()
2395
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002396 def GetGerritChange(self, patchset=None):
2397 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002398 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002399 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002400 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002401 data = self._GetChangeDetail(['ALL_REVISIONS'])
2402
2403 assert host and issue and patchset, 'CL must be uploaded first'
2404
2405 has_patchset = any(
2406 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002407 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002408 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002409 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002410 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002411
tandrii8c5a3532016-11-04 07:52:02 -07002412 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002413 'host': host,
2414 'change': issue,
2415 'project': data['project'],
2416 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002417 }
tandriie113dfd2016-10-11 10:20:12 -07002418
tandriide281ae2016-10-12 06:02:30 -07002419 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002420 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002421
Edward Lemur707d70b2018-02-07 00:50:14 +01002422 def GetReviewers(self):
2423 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002424 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002425
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002426
tandriif9aefb72016-07-01 09:06:51 -07002427def _get_bug_line_values(default_project, bugs):
2428 """Given default_project and comma separated list of bugs, yields bug line
2429 values.
2430
2431 Each bug can be either:
2432 * a number, which is combined with default_project
2433 * string, which is left as is.
2434
2435 This function may produce more than one line, because bugdroid expects one
2436 project per line.
2437
2438 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2439 ['v8:123', 'chromium:789']
2440 """
2441 default_bugs = []
2442 others = []
2443 for bug in bugs.split(','):
2444 bug = bug.strip()
2445 if bug:
2446 try:
2447 default_bugs.append(int(bug))
2448 except ValueError:
2449 others.append(bug)
2450
2451 if default_bugs:
2452 default_bugs = ','.join(map(str, default_bugs))
2453 if default_project:
2454 yield '%s:%s' % (default_project, default_bugs)
2455 else:
2456 yield default_bugs
2457 for other in sorted(others):
2458 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2459 yield other
2460
2461
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002462class ChangeDescription(object):
2463 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002464 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002465 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002466 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002467 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002468 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002469 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2470 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002471 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002472 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002473
Dan Beamd8b04ca2019-10-10 21:23:26 +00002474 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002475 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002476 if bug:
2477 regexp = re.compile(self.BUG_LINE)
2478 prefix = settings.GetBugPrefix()
2479 if not any((regexp.match(line) for line in self._description_lines)):
2480 values = list(_get_bug_line_values(prefix, bug))
2481 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002482 if fixed:
2483 regexp = re.compile(self.FIXED_LINE)
2484 prefix = settings.GetBugPrefix()
2485 if not any((regexp.match(line) for line in self._description_lines)):
2486 values = list(_get_bug_line_values(prefix, fixed))
2487 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002488
agable@chromium.org42c20792013-09-12 17:34:49 +00002489 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002490 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002491 return '\n'.join(self._description_lines)
2492
2493 def set_description(self, desc):
2494 if isinstance(desc, basestring):
2495 lines = desc.splitlines()
2496 else:
2497 lines = [line.rstrip() for line in desc]
2498 while lines and not lines[0]:
2499 lines.pop(0)
2500 while lines and not lines[-1]:
2501 lines.pop(-1)
2502 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002503
Edward Lemur5a644f82020-03-18 16:44:57 +00002504 def ensure_change_id(self, change_id):
2505 description = self.description
2506 footer_change_ids = git_footers.get_footer_change_id(description)
2507 # Make sure that the Change-Id in the description matches the given one.
2508 if footer_change_ids != [change_id]:
2509 if footer_change_ids:
2510 # Remove any existing Change-Id footers since they don't match the
2511 # expected change_id footer.
2512 description = git_footers.remove_footer(description, 'Change-Id')
2513 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
2514 'if you want to set a new one.')
2515 # Add the expected Change-Id footer.
2516 description = git_footers.add_footer_change_id(description, change_id)
2517 self.set_description(description)
2518
Edward Lemur2c62b332020-03-12 22:12:33 +00002519 def update_reviewers(
2520 self, reviewers, tbrs, add_owners_to, affected_files, author_email):
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002521 """Rewrites the R=/TBR= line(s) as a single line each.
2522
2523 Args:
2524 reviewers (list(str)) - list of additional emails to use for reviewers.
2525 tbrs (list(str)) - list of additional emails to use for TBRs.
2526 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2527 the change that are missing OWNER coverage. If this is not None, you
2528 must also pass a value for `change`.
2529 change (Change) - The Change that should be used for OWNERS lookups.
2530 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002531 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002532 assert isinstance(tbrs, list), tbrs
2533
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002534 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Edward Lemur2c62b332020-03-12 22:12:33 +00002535 assert not add_owners_to or affected_files, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002536
2537 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002538 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002539
2540 reviewers = set(reviewers)
2541 tbrs = set(tbrs)
2542 LOOKUP = {
2543 'TBR': tbrs,
2544 'R': reviewers,
2545 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002546
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002547 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002548 regexp = re.compile(self.R_LINE)
2549 matches = [regexp.match(line) for line in self._description_lines]
2550 new_desc = [l for i, l in enumerate(self._description_lines)
2551 if not matches[i]]
2552 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002553
agable@chromium.org42c20792013-09-12 17:34:49 +00002554 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002555
2556 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002557 for match in matches:
2558 if not match:
2559 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002560 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2561
2562 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002563 if add_owners_to:
Edward Lemur2c62b332020-03-12 22:12:33 +00002564 owners_db = owners.Database(settings.GetRoot(),
Edward Lemurb7f759f2020-03-04 21:20:56 +00002565 fopen=open, os_path=os.path)
Edward Lemur2c62b332020-03-12 22:12:33 +00002566 missing_files = owners_db.files_not_covered_by(affected_files,
Robert Iannucci100aa212017-04-18 17:28:26 -07002567 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002568 LOOKUP[add_owners_to].update(
Edward Lemur2c62b332020-03-12 22:12:33 +00002569 owners_db.reviewers_for(missing_files, author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002570
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002571 # If any folks ended up in both groups, remove them from tbrs.
2572 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002573
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002574 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2575 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002576
2577 # Put the new lines in the description where the old first R= line was.
2578 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2579 if 0 <= line_loc < len(self._description_lines):
2580 if new_tbr_line:
2581 self._description_lines.insert(line_loc, new_tbr_line)
2582 if new_r_line:
2583 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002584 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002585 if new_r_line:
2586 self.append_footer(new_r_line)
2587 if new_tbr_line:
2588 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002589
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002590 def set_preserve_tryjobs(self):
2591 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2592 footers = git_footers.parse_footers(self.description)
2593 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2594 if v.lower() == 'true':
2595 return
2596 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2597
Anthony Polito8b955342019-09-24 19:01:36 +00002598 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002599 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002600 self.set_description([
2601 '# Enter a description of the change.',
2602 '# This will be displayed on the codereview site.',
2603 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002604 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002605 '--------------------',
2606 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002607 bug_regexp = re.compile(self.BUG_LINE)
2608 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002609 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002610 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2611 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002612 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002613
agable@chromium.org42c20792013-09-12 17:34:49 +00002614 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002615 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002616 if not content:
2617 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002618 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002619
Bruce Dawson2377b012018-01-11 16:46:49 -08002620 # Strip off comments and default inserted "Bug:" line.
2621 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002622 (line.startswith('#') or
2623 line.rstrip() == "Bug:" or
2624 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002625 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002626 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002627 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002628
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002629 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002630 """Adds a footer line to the description.
2631
2632 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2633 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2634 that Gerrit footers are always at the end.
2635 """
2636 parsed_footer_line = git_footers.parse_footer(line)
2637 if parsed_footer_line:
2638 # Line is a gerrit footer in the form: Footer-Key: any value.
2639 # Thus, must be appended observing Gerrit footer rules.
2640 self.set_description(
2641 git_footers.add_footer(self.description,
2642 key=parsed_footer_line[0],
2643 value=parsed_footer_line[1]))
2644 return
2645
2646 if not self._description_lines:
2647 self._description_lines.append(line)
2648 return
2649
2650 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2651 if gerrit_footers:
2652 # git_footers.split_footers ensures that there is an empty line before
2653 # actual (gerrit) footers, if any. We have to keep it that way.
2654 assert top_lines and top_lines[-1] == ''
2655 top_lines, separator = top_lines[:-1], top_lines[-1:]
2656 else:
2657 separator = [] # No need for separator if there are no gerrit_footers.
2658
2659 prev_line = top_lines[-1] if top_lines else ''
2660 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2661 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2662 top_lines.append('')
2663 top_lines.append(line)
2664 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002665
tandrii99a72f22016-08-17 14:33:24 -07002666 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002667 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002668 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002669 reviewers = [match.group(2).strip()
2670 for match in matches
2671 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002672 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002673
bradnelsond975b302016-10-23 12:20:23 -07002674 def get_cced(self):
2675 """Retrieves the list of reviewers."""
2676 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2677 cced = [match.group(2).strip() for match in matches if match]
2678 return cleanup_list(cced)
2679
Nodir Turakulov23b82142017-11-16 11:04:25 -08002680 def get_hash_tags(self):
2681 """Extracts and sanitizes a list of Gerrit hashtags."""
2682 subject = (self._description_lines or ('',))[0]
2683 subject = re.sub(
2684 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2685
2686 tags = []
2687 start = 0
2688 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2689 while True:
2690 m = bracket_exp.match(subject, start)
2691 if not m:
2692 break
2693 tags.append(self.sanitize_hash_tag(m.group(1)))
2694 start = m.end()
2695
2696 if not tags:
2697 # Try "Tag: " prefix.
2698 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
2699 if m:
2700 tags.append(self.sanitize_hash_tag(m.group(1)))
2701 return tags
2702
2703 @classmethod
2704 def sanitize_hash_tag(cls, tag):
2705 """Returns a sanitized Gerrit hash tag.
2706
2707 A sanitized hashtag can be used as a git push refspec parameter value.
2708 """
2709 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
2710
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712def FindCodereviewSettingsFile(filename='codereview.settings'):
2713 """Finds the given file starting in the cwd and going up.
2714
2715 Only looks up to the top of the repository unless an
2716 'inherit-review-settings-ok' file exists in the root of the repository.
2717 """
2718 inherit_ok_file = 'inherit-review-settings-ok'
2719 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002720 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2722 root = '/'
2723 while True:
2724 if filename in os.listdir(cwd):
2725 if os.path.isfile(os.path.join(cwd, filename)):
2726 return open(os.path.join(cwd, filename))
2727 if cwd == root:
2728 break
2729 cwd = os.path.dirname(cwd)
2730
2731
2732def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002733 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002734 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736 def SetProperty(name, setting, unset_error_ok=False):
2737 fullname = 'rietveld.' + name
2738 if setting in keyvals:
2739 RunGit(['config', fullname, keyvals[setting]])
2740 else:
2741 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2742
tandrii48df5812016-10-17 03:55:37 -07002743 if not keyvals.get('GERRIT_HOST', False):
2744 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002745 # Only server setting is required. Other settings can be absent.
2746 # In that case, we ignore errors raised during option deletion attempt.
2747 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
2748 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2749 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002750 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002751 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
2752 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002753 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2754 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00002755 SetProperty(
2756 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002757
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002758 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002759 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002760
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002761 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002762 RunGit(['config', 'gerrit.squash-uploads',
2763 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002764
tandrii@chromium.org28253532016-04-14 13:46:56 +00002765 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002766 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002767 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2768
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002769 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002770 # should be of the form
2771 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2772 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2774 keyvals['ORIGIN_URL_CONFIG']])
2775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002777def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002778 """Downloads a network object to a local file, like urllib.urlretrieve.
2779
2780 This is necessary because urllib is broken for SSL connections via a proxy.
2781 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002782 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00002783 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002784
2785
ukai@chromium.org712d6102013-11-27 00:52:58 +00002786def hasSheBang(fname):
2787 """Checks fname is a #! script."""
2788 with open(fname) as f:
2789 return f.read(2).startswith('#!')
2790
2791
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002792def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002793 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002794
2795 Args:
2796 force: True to update hooks. False to install hooks if not present.
2797 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00002798 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002799 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2800 if not os.access(dst, os.X_OK):
2801 if os.path.exists(dst):
2802 if not force:
2803 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002804 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002805 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002806 if not hasSheBang(dst):
2807 DieWithError('Not a script: %s\n'
2808 'You need to download from\n%s\n'
2809 'into .git/hooks/commit-msg and '
2810 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002811 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2812 except Exception:
2813 if os.path.exists(dst):
2814 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002815 DieWithError('\nFailed to download hooks.\n'
2816 'You need to download from\n%s\n'
2817 'into .git/hooks/commit-msg and '
2818 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002819
2820
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002821class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002822 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002823
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002824 _GOOGLESOURCE = 'googlesource.com'
2825
2826 def __init__(self):
2827 # Cached list of [host, identity, source], where source is either
2828 # .gitcookies or .netrc.
2829 self._all_hosts = None
2830
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002831 def ensure_configured_gitcookies(self):
2832 """Runs checks and suggests fixes to make git use .gitcookies from default
2833 path."""
2834 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
2835 configured_path = RunGitSilent(
2836 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02002837 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002838 if configured_path:
2839 self._ensure_default_gitcookies_path(configured_path, default)
2840 else:
2841 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002842
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002843 @staticmethod
2844 def _ensure_default_gitcookies_path(configured_path, default_path):
2845 assert configured_path
2846 if configured_path == default_path:
2847 print('git is already configured to use your .gitcookies from %s' %
2848 configured_path)
2849 return
2850
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002851 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002852 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
2853 (configured_path, default_path))
2854
2855 if not os.path.exists(configured_path):
2856 print('However, your configured .gitcookies file is missing.')
2857 confirm_or_exit('Reconfigure git to use default .gitcookies?',
2858 action='reconfigure')
2859 RunGit(['config', '--global', 'http.cookiefile', default_path])
2860 return
2861
2862 if os.path.exists(default_path):
2863 print('WARNING: default .gitcookies file already exists %s' %
2864 default_path)
2865 DieWithError('Please delete %s manually and re-run git cl creds-check' %
2866 default_path)
2867
2868 confirm_or_exit('Move existing .gitcookies to default location?',
2869 action='move')
2870 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002871 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002872 print('Moved and reconfigured git to use .gitcookies from %s' %
2873 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002874
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01002875 @staticmethod
2876 def _configure_gitcookies_path(default_path):
2877 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
2878 if os.path.exists(netrc_path):
2879 print('You seem to be using outdated .netrc for git credentials: %s' %
2880 netrc_path)
2881 print('This tool will guide you through setting up recommended '
2882 '.gitcookies store for git credentials.\n'
2883 '\n'
2884 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
2885 ' git config --global --unset http.cookiefile\n'
2886 ' mv %s %s.backup\n\n' % (default_path, default_path))
2887 confirm_or_exit(action='setup .gitcookies')
2888 RunGit(['config', '--global', 'http.cookiefile', default_path])
2889 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01002890
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002891 def get_hosts_with_creds(self, include_netrc=False):
2892 if self._all_hosts is None:
2893 a = gerrit_util.CookiesAuthenticator()
2894 self._all_hosts = [
2895 (h, u, s)
2896 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002897 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
2898 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002899 )
2900 if h.endswith(self._GOOGLESOURCE)
2901 ]
2902
2903 if include_netrc:
2904 return self._all_hosts
2905 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
2906
2907 def print_current_creds(self, include_netrc=False):
2908 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
2909 if not hosts:
2910 print('No Git/Gerrit credentials found')
2911 return
Edward Lemur79d4f992019-11-11 23:49:02 +00002912 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01002913 header = [('Host', 'User', 'Which file'),
2914 ['=' * l for l in lengths]]
2915 for row in (header + hosts):
2916 print('\t'.join((('%%+%ds' % l) % s)
2917 for l, s in zip(lengths, row)))
2918
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002919 @staticmethod
2920 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08002921 """Parses identity "git-<username>.domain" into <username> and domain."""
2922 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002923 # distinguishable from sub-domains. But we do know typical domains:
2924 if identity.endswith('.chromium.org'):
2925 domain = 'chromium.org'
2926 username = identity[:-len('.chromium.org')]
2927 else:
2928 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002929 if username.startswith('git-'):
2930 username = username[len('git-'):]
2931 return username, domain
2932
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002933 def _canonical_git_googlesource_host(self, host):
2934 """Normalizes Gerrit hosts (with '-review') to Git host."""
2935 assert host.endswith(self._GOOGLESOURCE)
2936 # Prefix doesn't include '.' at the end.
2937 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
2938 if prefix.endswith('-review'):
2939 prefix = prefix[:-len('-review')]
2940 return prefix + '.' + self._GOOGLESOURCE
2941
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01002942 def _canonical_gerrit_googlesource_host(self, host):
2943 git_host = self._canonical_git_googlesource_host(host)
2944 prefix = git_host.split('.', 1)[0]
2945 return prefix + '-review.' + self._GOOGLESOURCE
2946
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002947 def _get_counterpart_host(self, host):
2948 assert host.endswith(self._GOOGLESOURCE)
2949 git = self._canonical_git_googlesource_host(host)
2950 gerrit = self._canonical_gerrit_googlesource_host(git)
2951 return git if gerrit == host else gerrit
2952
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002953 def has_generic_host(self):
2954 """Returns whether generic .googlesource.com has been configured.
2955
2956 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
2957 """
2958 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
2959 if host == '.' + self._GOOGLESOURCE:
2960 return True
2961 return False
2962
2963 def _get_git_gerrit_identity_pairs(self):
2964 """Returns map from canonic host to pair of identities (Git, Gerrit).
2965
2966 One of identities might be None, meaning not configured.
2967 """
2968 host_to_identity_pairs = {}
2969 for host, identity, _ in self.get_hosts_with_creds():
2970 canonical = self._canonical_git_googlesource_host(host)
2971 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
2972 idx = 0 if canonical == host else 1
2973 pair[idx] = identity
2974 return host_to_identity_pairs
2975
2976 def get_partially_configured_hosts(self):
2977 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002978 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002979 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002980 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002981
2982 def get_conflicting_hosts(self):
2983 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02002984 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002985 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002986 if None not in (i1, i2) and i1 != i2)
2987
2988 def get_duplicated_hosts(self):
2989 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002990 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01002991
2992 _EXPECTED_HOST_IDENTITY_DOMAINS = {
2993 'chromium.googlesource.com': 'chromium.org',
2994 'chrome-internal.googlesource.com': 'google.com',
2995 }
2996
2997 def get_hosts_with_wrong_identities(self):
2998 """Finds hosts which **likely** reference wrong identities.
2999
3000 Note: skips hosts which have conflicting identities for Git and Gerrit.
3001 """
3002 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003003 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003004 pair = self._get_git_gerrit_identity_pairs().get(host)
3005 if pair and pair[0] == pair[1]:
3006 _, domain = self._parse_identity(pair[0])
3007 if domain != expected:
3008 hosts.add(host)
3009 return hosts
3010
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003011 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003012 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003013 hosts = sorted(hosts)
3014 assert hosts
3015 if extra_column_func is None:
3016 extras = [''] * len(hosts)
3017 else:
3018 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003019 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3020 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003021 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003022 lines.append(tmpl % he)
3023 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003024
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003025 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003026 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003027 yield ('.googlesource.com wildcard record detected',
3028 ['Chrome Infrastructure team recommends to list full host names '
3029 'explicitly.'],
3030 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003031
3032 dups = self.get_duplicated_hosts()
3033 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003034 yield ('The following hosts were defined twice',
3035 self._format_hosts(dups),
3036 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003037
3038 partial = self.get_partially_configured_hosts()
3039 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003040 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3041 'These hosts are missing',
3042 self._format_hosts(partial, lambda host: 'but %s defined' %
3043 self._get_counterpart_host(host)),
3044 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003045
3046 conflicting = self.get_conflicting_hosts()
3047 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003048 yield ('The following Git hosts have differing credentials from their '
3049 'Gerrit counterparts',
3050 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3051 tuple(self._get_git_gerrit_identity_pairs()[host])),
3052 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003053
3054 wrong = self.get_hosts_with_wrong_identities()
3055 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003056 yield ('These hosts likely use wrong identity',
3057 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3058 (self._get_git_gerrit_identity_pairs()[host][0],
3059 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3060 wrong)
3061
3062 def find_and_report_problems(self):
3063 """Returns True if there was at least one problem, else False."""
3064 found = False
3065 bad_hosts = set()
3066 for title, sublines, hosts in self._find_problems():
3067 if not found:
3068 found = True
3069 print('\n\n.gitcookies problem report:\n')
3070 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003071 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003072 if sublines:
3073 print()
3074 print(' %s' % '\n '.join(sublines))
3075 print()
3076
3077 if bad_hosts:
3078 assert found
3079 print(' You can manually remove corresponding lines in your %s file and '
3080 'visit the following URLs with correct account to generate '
3081 'correct credential lines:\n' %
3082 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3083 print(' %s' % '\n '.join(sorted(set(
3084 gerrit_util.CookiesAuthenticator().get_new_password_url(
3085 self._canonical_git_googlesource_host(host))
3086 for host in bad_hosts
3087 ))))
3088 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003089
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003090
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003091@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003092def CMDcreds_check(parser, args):
3093 """Checks credentials and suggests changes."""
3094 _, _ = parser.parse_args(args)
3095
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003096 # Code below checks .gitcookies. Abort if using something else.
3097 authn = gerrit_util.Authenticator.get()
3098 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003099 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003100 'This command is not designed for bot environment. It checks '
3101 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003102 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3103 if isinstance(authn, gerrit_util.GceAuthenticator):
3104 message += (
3105 '\n'
3106 'If you need to run this on GCE or a cloudtop instance, '
3107 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3108 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003109
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003110 checker = _GitCookiesChecker()
3111 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003112
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003113 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003114 checker.print_current_creds(include_netrc=True)
3115
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003116 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003117 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003118 return 0
3119 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003120
3121
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003122@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003123def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003124 """Gets or sets base-url for this branch."""
Edward Lesmes50da7702020-03-30 19:23:43 +00003125 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003126 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003127 _, args = parser.parse_args(args)
3128 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003129 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003130 return RunGit(['config', 'branch.%s.base-url' % branch],
3131 error_ok=False).strip()
3132 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003133 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003134 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3135 error_ok=False).strip()
3136
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003137
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003138def color_for_status(status):
3139 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003140 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003141 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003142 'unsent': BOLD + Fore.YELLOW,
3143 'waiting': BOLD + Fore.RED,
3144 'reply': BOLD + Fore.YELLOW,
3145 'not lgtm': BOLD + Fore.RED,
3146 'lgtm': BOLD + Fore.GREEN,
3147 'commit': BOLD + Fore.MAGENTA,
3148 'closed': BOLD + Fore.CYAN,
3149 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003150 }.get(status, Fore.WHITE)
3151
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003152
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003153def get_cl_statuses(changes, fine_grained, max_processes=None):
3154 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003155
3156 If fine_grained is true, this will fetch CL statuses from the server.
3157 Otherwise, simply indicate if there's a matching url for the given branches.
3158
3159 If max_processes is specified, it is used as the maximum number of processes
3160 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3161 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003162
3163 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003164 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003165 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003166 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003167
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003168 if not fine_grained:
3169 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003170 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003171 for cl in changes:
3172 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003173 return
3174
3175 # First, sort out authentication issues.
3176 logging.debug('ensuring credentials exist')
3177 for cl in changes:
3178 cl.EnsureAuthenticated(force=False, refresh=True)
3179
3180 def fetch(cl):
3181 try:
3182 return (cl, cl.GetStatus())
3183 except:
3184 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003185 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003186 raise
3187
3188 threads_count = len(changes)
3189 if max_processes:
3190 threads_count = max(1, min(threads_count, max_processes))
3191 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3192
Edward Lemur61bf4172020-02-24 23:22:37 +00003193 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003194 fetched_cls = set()
3195 try:
3196 it = pool.imap_unordered(fetch, changes).__iter__()
3197 while True:
3198 try:
3199 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003200 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003201 break
3202 fetched_cls.add(cl)
3203 yield cl, status
3204 finally:
3205 pool.close()
3206
3207 # Add any branches that failed to fetch.
3208 for cl in set(changes) - fetched_cls:
3209 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003210
rmistry@google.com2dd99862015-06-22 12:22:18 +00003211
Jose Lopes3863fc52020-04-07 17:00:25 +00003212def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003213 """Uploads CLs of local branches that are dependents of the current branch.
3214
3215 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003216
3217 test1 -> test2.1 -> test3.1
3218 -> test3.2
3219 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003220
3221 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3222 run on the dependent branches in this order:
3223 test2.1, test3.1, test3.2, test2.2, test3.3
3224
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003225 Note: This function does not rebase your local dependent branches. Use it
3226 when you make a change to the parent branch that will not conflict
3227 with its dependent branches, and you would like their dependencies
3228 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003229 """
3230 if git_common.is_dirty_git_tree('upload-branch-deps'):
3231 return 1
3232
3233 root_branch = cl.GetBranch()
3234 if root_branch is None:
3235 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3236 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003237 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003238 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3239 'patchset dependencies without an uploaded CL.')
3240
3241 branches = RunGit(['for-each-ref',
3242 '--format=%(refname:short) %(upstream:short)',
3243 'refs/heads'])
3244 if not branches:
3245 print('No local branches found.')
3246 return 0
3247
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003248 # Create a dictionary of all local branches to the branches that are
3249 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003250 tracked_to_dependents = collections.defaultdict(list)
3251 for b in branches.splitlines():
3252 tokens = b.split()
3253 if len(tokens) == 2:
3254 branch_name, tracked = tokens
3255 tracked_to_dependents[tracked].append(branch_name)
3256
vapiera7fbd5a2016-06-16 09:17:49 -07003257 print()
3258 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003259 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003260
rmistry@google.com2dd99862015-06-22 12:22:18 +00003261 def traverse_dependents_preorder(branch, padding=''):
3262 dependents_to_process = tracked_to_dependents.get(branch, [])
3263 padding += ' '
3264 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003265 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003266 dependents.append(dependent)
3267 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003268
rmistry@google.com2dd99862015-06-22 12:22:18 +00003269 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003270 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003271
3272 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003273 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003274 return 0
3275
Jose Lopes3863fc52020-04-07 17:00:25 +00003276 if not force:
3277 confirm_or_exit('This command will checkout all dependent branches and run '
3278 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003279
rmistry@google.com2dd99862015-06-22 12:22:18 +00003280 # Record all dependents that failed to upload.
3281 failures = {}
3282 # Go through all dependents, checkout the branch and upload.
3283 try:
3284 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003285 print()
3286 print('--------------------------------------')
3287 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003288 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003289 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003290 try:
3291 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003292 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003293 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003294 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003295 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003296 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003297 finally:
3298 # Swap back to the original root branch.
3299 RunGit(['checkout', '-q', root_branch])
3300
vapiera7fbd5a2016-06-16 09:17:49 -07003301 print()
3302 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003303 for dependent_branch in dependents:
3304 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print(' %s : %s' % (dependent_branch, upload_status))
3306 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003307
3308 return 0
3309
3310
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003311def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003312 """Given a proposed tag name, returns a tag name that is guaranteed to be
3313 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3314 or 'foo-3', and so on."""
3315
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003316 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003317 for suffix_num in itertools.count(1):
3318 if suffix_num == 1:
3319 to_check = proposed_tag
3320 else:
3321 to_check = '%s-%d' % (proposed_tag, suffix_num)
3322
3323 if to_check not in existing_tags:
3324 return to_check
3325
3326
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003327@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003328def CMDarchive(parser, args):
3329 """Archives and deletes branches associated with closed changelists."""
3330 parser.add_option(
3331 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003332 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003333 parser.add_option(
3334 '-f', '--force', action='store_true',
3335 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003336 parser.add_option(
3337 '-d', '--dry-run', action='store_true',
3338 help='Skip the branch tagging and removal steps.')
3339 parser.add_option(
3340 '-t', '--notags', action='store_true',
3341 help='Do not tag archived branches. '
3342 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003343 parser.add_option(
3344 '-p',
3345 '--pattern',
3346 default='git-cl-archived-{issue}-{branch}',
3347 help='Format string for archive tags. '
3348 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003349
kmarshall3bff56b2016-06-06 18:31:47 -07003350 options, args = parser.parse_args(args)
3351 if args:
3352 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003353
3354 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3355 if not branches:
3356 return 0
3357
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003358 tags = RunGit(['for-each-ref', '--format=%(refname)',
3359 'refs/tags']).splitlines() or []
3360 tags = [t.split('/')[-1] for t in tags]
3361
vapiera7fbd5a2016-06-16 09:17:49 -07003362 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003363 changes = [Changelist(branchref=b)
3364 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003365 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3366 statuses = get_cl_statuses(changes,
3367 fine_grained=True,
3368 max_processes=options.maxjobs)
3369 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003370 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3371 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003372 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003373 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003374 proposal.sort()
3375
3376 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003377 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003378 return 0
3379
Edward Lemur85153282020-02-14 22:06:29 +00003380 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003381
vapiera7fbd5a2016-06-16 09:17:49 -07003382 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003383 if options.notags:
3384 for next_item in proposal:
3385 print(' ' + next_item[0])
3386 else:
3387 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3388 for next_item in proposal:
3389 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003390
kmarshall9249e012016-08-23 12:02:16 -07003391 # Quit now on precondition failure or if instructed by the user, either
3392 # via an interactive prompt or by command line flags.
3393 if options.dry_run:
3394 print('\nNo changes were made (dry run).\n')
3395 return 0
3396 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003397 print('You are currently on a branch \'%s\' which is associated with a '
3398 'closed codereview issue, so archive cannot proceed. Please '
3399 'checkout another branch and run this command again.' %
3400 current_branch)
3401 return 1
kmarshall9249e012016-08-23 12:02:16 -07003402 elif not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00003403 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07003404 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003405 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003406 return 1
3407
3408 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003409 if not options.notags:
3410 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003411
3412 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3413 # Clean up the tag if we failed to delete the branch.
3414 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003415
vapiera7fbd5a2016-06-16 09:17:49 -07003416 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003417
3418 return 0
3419
3420
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003421@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003422def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003423 """Show status of changelists.
3424
3425 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003426 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003427 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003428 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003429 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003430 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003431 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003432 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003433
3434 Also see 'git cl comments'.
3435 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003436 parser.add_option(
3437 '--no-branch-color',
3438 action='store_true',
3439 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003440 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003441 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003442 parser.add_option('-f', '--fast', action='store_true',
3443 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003444 parser.add_option(
3445 '-j', '--maxjobs', action='store', type=int,
3446 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003447 parser.add_option(
3448 '-i', '--issue', type=int,
3449 help='Operate on this issue instead of the current branch\'s implicit '
3450 'issue. Requires --field to be set.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003451 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003452 if args:
3453 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454
iannuccie53c9352016-08-17 14:40:40 -07003455 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003456 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003458 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003459 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003460 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003461 if cl.GetIssue():
3462 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003463 elif options.field == 'id':
3464 issueid = cl.GetIssue()
3465 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003468 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003469 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003471 elif options.field == 'status':
3472 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473 elif options.field == 'url':
3474 url = cl.GetIssueURL()
3475 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003476 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003477 return 0
3478
3479 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3480 if not branches:
3481 print('No local branch found.')
3482 return 0
3483
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003484 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003485 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003486 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003487 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003489 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003490 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003491
Edward Lemur85153282020-02-14 22:06:29 +00003492 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00003493
3494 def FormatBranchName(branch, colorize=False):
3495 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3496 an asterisk when it is the current branch."""
3497
3498 asterisk = ""
3499 color = Fore.RESET
3500 if branch == current_branch:
3501 asterisk = "* "
3502 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00003503 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00003504
3505 if colorize:
3506 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003507 return asterisk + branch_name
3508
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003509 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003510
3511 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003512 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3513 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003514 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003515 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003516 branch_statuses[c.GetBranch()] = status
3517 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00003518 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003519 if url and (not status or status == 'error'):
3520 # The issue probably doesn't exist anymore.
3521 url += ' (broken)'
3522
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003523 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003524 # Turn off bold as well as colors.
3525 END = '\033[0m'
3526 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003527 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003528 color = ''
3529 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003530 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003531
Alan Cuttera3be9a52019-03-04 18:50:33 +00003532 branch_display = FormatBranchName(branch)
3533 padding = ' ' * (alignment - len(branch_display))
3534 if not options.no_branch_color:
3535 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003536
Alan Cuttera3be9a52019-03-04 18:50:33 +00003537 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3538 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003539
vapiera7fbd5a2016-06-16 09:17:49 -07003540 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003541 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003542 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003543 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003544 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003545 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003546 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003547 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003548 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003549 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003550 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003551 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552 return 0
3553
3554
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003555def colorize_CMDstatus_doc():
3556 """To be called once in main() to add colors to git cl status help."""
3557 colors = [i for i in dir(Fore) if i[0].isupper()]
3558
3559 def colorize_line(line):
3560 for color in colors:
3561 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003562 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003563 indent = len(line) - len(line.lstrip(' ')) + 1
3564 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3565 return line
3566
3567 lines = CMDstatus.__doc__.splitlines()
3568 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3569
3570
phajdan.jre328cf92016-08-22 04:12:17 -07003571def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003572 if path == '-':
3573 json.dump(contents, sys.stdout)
3574 else:
3575 with open(path, 'w') as f:
3576 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003577
3578
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003579@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003580@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003581def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003582 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583
3584 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003585 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003586 parser.add_option('-r', '--reverse', action='store_true',
3587 help='Lookup the branch(es) for the specified issues. If '
3588 'no issues are specified, all branches with mapped '
3589 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003590 parser.add_option('--json',
3591 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003592 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003593
dnj@chromium.org406c4402015-03-03 17:22:28 +00003594 if options.reverse:
3595 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003596 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003597 # Reverse issue lookup.
3598 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003599
3600 git_config = {}
3601 for config in RunGit(['config', '--get-regexp',
3602 r'branch\..*issue']).splitlines():
3603 name, _space, val = config.partition(' ')
3604 git_config[name] = val
3605
dnj@chromium.org406c4402015-03-03 17:22:28 +00003606 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00003607 issue = git_config.get(
3608 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00003609 if issue:
3610 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003611 if not args:
3612 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003613 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003614 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003615 try:
3616 issue_num = int(issue)
3617 except ValueError:
3618 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003619 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003620 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003621 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003622 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003623 if options.json:
3624 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003625 return 0
3626
3627 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003628 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003629 if not issue.valid:
3630 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3631 'or no argument to list it.\n'
3632 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003633 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003634 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003635 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003636 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003637 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
3638 if options.json:
3639 write_json(options.json, {
3640 'issue': cl.GetIssue(),
3641 'issue_url': cl.GetIssueURL(),
3642 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003643 return 0
3644
3645
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003646@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003647def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003648 """Shows or posts review comments for any changelist."""
3649 parser.add_option('-a', '--add-comment', dest='comment',
3650 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003651 parser.add_option('-p', '--publish', action='store_true',
3652 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01003653 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00003654 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003655 parser.add_option('-m', '--machine-readable', dest='readable',
3656 action='store_false', default=True,
3657 help='output comments in a format compatible with '
3658 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00003659 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07003660 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003661 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003662
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003663 issue = None
3664 if options.issue:
3665 try:
3666 issue = int(options.issue)
3667 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003668 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003669
Edward Lemur934836a2019-09-09 20:16:54 +00003670 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003671
3672 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003673 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003674 return 0
3675
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003676 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
3677 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003678 for comment in summary:
3679 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003680 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003681 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003682 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003683 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003684 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00003685 elif comment.autogenerated:
3686 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003687 else:
3688 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003689 print('\n%s%s %s%s\n%s' % (
3690 color,
3691 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
3692 comment.sender,
3693 Fore.RESET,
3694 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
3695
smut@google.comc85ac942015-09-15 16:34:43 +00003696 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003697 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00003698 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003699 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
3700 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00003701 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003702 return 0
3703
3704
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003705@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003706@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003707def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003708 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003709 parser.add_option('-d', '--display', action='store_true',
3710 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003711 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003712 help='New description to set for this issue (- for stdin, '
3713 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003714 parser.add_option('-f', '--force', action='store_true',
3715 help='Delete any unpublished Gerrit edits for this issue '
3716 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003717
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003718 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003719
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003720 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003721 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003722 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003723 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003724 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003725
Edward Lemur934836a2019-09-09 20:16:54 +00003726 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003727 if target_issue_arg:
3728 kwargs['issue'] = target_issue_arg.issue
3729 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003730
3731 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003732 if not cl.GetIssue():
3733 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003734
Edward Lemur678a6842019-10-03 22:25:05 +00003735 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00003736 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003737
Edward Lemur6c6827c2020-02-06 21:15:18 +00003738 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003739
smut@google.com34fb6b12015-07-13 20:03:26 +00003740 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003741 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003742 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003743
3744 if options.new_description:
3745 text = options.new_description
3746 if text == '-':
3747 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003748 elif text == '+':
3749 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00003750 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003751
3752 description.set_description(text)
3753 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003754 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00003755 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003756 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003757 return 0
3758
3759
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003760@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003761def CMDlint(parser, args):
3762 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003763 parser.add_option('--filter', action='append', metavar='-x,+y',
3764 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003765 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003766
3767 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003768 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00003769 try:
3770 import cpplint
3771 import cpplint_chromium
3772 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003773 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003774 return 1
3775
3776 # Change the current working directory before calling lint so that it
3777 # shows the correct base.
3778 previous_cwd = os.getcwd()
3779 os.chdir(settings.GetRoot())
3780 try:
Edward Lemur934836a2019-09-09 20:16:54 +00003781 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00003782 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003783 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003784 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003785 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003786
3787 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003788 command = args + files
3789 if options.filter:
3790 command = ['--filter=' + ','.join(options.filter)] + command
3791 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003792
3793 white_regex = re.compile(settings.GetLintRegex())
3794 black_regex = re.compile(settings.GetLintIgnoreRegex())
3795 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3796 for filename in filenames:
3797 if white_regex.match(filename):
3798 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003799 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003800 else:
3801 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3802 extra_check_functions)
3803 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003804 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003805 finally:
3806 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003807 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003808 if cpplint._cpplint_state.error_count != 0:
3809 return 1
3810 return 0
3811
3812
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003813@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003814def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003815 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003816 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003817 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003818 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003819 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08003820 parser.add_option('--all', action='store_true',
3821 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04003822 parser.add_option('--parallel', action='store_true',
3823 help='Run all tests specified by input_api.RunTests in all '
3824 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003825 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826
sbc@chromium.org71437c02015-04-09 19:29:40 +00003827 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003828 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829 return 1
3830
Edward Lemur934836a2019-09-09 20:16:54 +00003831 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832 if args:
3833 base_branch = args[0]
3834 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003835 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003836 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837
Gregory Nisbet29d5cf82020-02-27 08:16:58 +00003838 if cl.GetIssue():
3839 description = cl.FetchDescription()
Aaron Gable8076c282017-11-29 14:39:41 -08003840 else:
Edward Lemura12175c2020-03-09 16:58:26 +00003841 description = _create_description_from_log([base_branch])
Aaron Gable8076c282017-11-29 14:39:41 -08003842
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003843 cl.RunHook(
3844 committing=not options.upload,
3845 may_prompt=False,
3846 verbose=options.verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00003847 parallel=options.parallel,
3848 upstream=base_branch,
3849 description=description,
3850 all_files=options.all)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003851 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003852
3853
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003854def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003855 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003856
3857 Works the same way as
3858 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3859 but can be called on demand on all platforms.
3860
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003861 The basic idea is to generate git hash of a state of the tree, original
3862 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003863 """
3864 lines = []
3865 tree_hash = RunGitSilent(['write-tree'])
3866 lines.append('tree %s' % tree_hash.strip())
3867 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3868 if code == 0:
3869 lines.append('parent %s' % parent.strip())
3870 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3871 lines.append('author %s' % author.strip())
3872 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3873 lines.append('committer %s' % committer.strip())
3874 lines.append('')
3875 # Note: Gerrit's commit-hook actually cleans message of some lines and
3876 # whitespace. This code is not doing this, but it clearly won't decrease
3877 # entropy.
3878 lines.append(message)
3879 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00003880 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003881 return 'I%s' % change_hash.strip()
3882
3883
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01003884def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00003885 """Computes the remote branch ref to use for the CL.
3886
3887 Args:
3888 remote (str): The git remote for the CL.
3889 remote_branch (str): The git remote branch for the CL.
3890 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00003891 """
3892 if not (remote and remote_branch):
3893 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003894
wittman@chromium.org455dc922015-01-26 20:15:50 +00003895 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003896 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00003897 # refs, which are then translated into the remote full symbolic refs
3898 # below.
3899 if '/' not in target_branch:
3900 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3901 else:
3902 prefix_replacements = (
3903 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3904 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3905 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3906 )
3907 match = None
3908 for regex, replacement in prefix_replacements:
3909 match = re.search(regex, target_branch)
3910 if match:
3911 remote_branch = target_branch.replace(match.group(0), replacement)
3912 break
3913 if not match:
3914 # This is a branch path but not one we recognize; use as-is.
3915 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003916 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3917 # Handle the refs that need to land in different refs.
3918 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003919
wittman@chromium.org455dc922015-01-26 20:15:50 +00003920 # Create the true path to the remote branch.
3921 # Does the following translation:
3922 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3923 # * refs/remotes/origin/master -> refs/heads/master
3924 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3925 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3926 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3927 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3928 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3929 'refs/heads/')
3930 elif remote_branch.startswith('refs/remotes/branch-heads'):
3931 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01003932
wittman@chromium.org455dc922015-01-26 20:15:50 +00003933 return remote_branch
3934
3935
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003936def cleanup_list(l):
3937 """Fixes a list so that comma separated items are put as individual items.
3938
3939 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3940 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3941 """
3942 items = sum((i.split(',') for i in l), [])
3943 stripped_items = (i.strip() for i in items)
3944 return sorted(filter(None, stripped_items))
3945
3946
Aaron Gable4db38df2017-11-03 14:59:07 -07003947@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003948@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003949def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003950 """Uploads the current changelist to codereview.
3951
3952 Can skip dependency patchset uploads for a branch by running:
3953 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003954 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00003955 git config --unset branch.branch_name.skip-deps-uploads
3956 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02003957
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003958 If the name of the checked out branch starts with "bug-" or "fix-" followed
3959 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02003960 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003961
3962 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003963 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003964 [git-cl] add support for hashtags
3965 Foo bar: implement foo
3966 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00003967 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003968 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3969 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003970 parser.add_option('--bypass-watchlists', action='store_true',
3971 dest='bypass_watchlists',
3972 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07003973 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003974 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003975 parser.add_option('--message', '-m', dest='message',
3976 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003977 parser.add_option('-b', '--bug',
3978 help='pre-populate the bug number(s) for this issue. '
3979 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003980 parser.add_option('--message-file', dest='message_file',
3981 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08003982 parser.add_option('--title', '-t', dest='title',
3983 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003984 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003985 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003986 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003987 parser.add_option('--tbrs',
3988 action='append', default=[],
3989 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003990 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003991 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003992 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003993 parser.add_option('--hashtag', dest='hashtags',
3994 action='append', default=[],
3995 help=('Gerrit hashtag for new CL; '
3996 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00003997 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08003998 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003999 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004000 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004001 metavar='TARGET',
4002 help='Apply CL to remote ref TARGET. ' +
4003 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004004 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004005 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004006 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004007 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004008 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004009 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004010 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4011 const='TBR', help='add a set of OWNERS to TBR')
4012 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4013 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004014 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004015 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004016 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004017 'implies --send-mail')
4018 parser.add_option('-d', '--cq-dry-run',
4019 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004020 help='Send the patchset to do a CQ dry run right after '
4021 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004022 parser.add_option('--preserve-tryjobs', action='store_true',
4023 help='instruct the CQ to let tryjobs running even after '
4024 'new patchsets are uploaded instead of canceling '
4025 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004026 parser.add_option('--dependencies', action='store_true',
4027 help='Uploads CLs of all the local branches that depend on '
4028 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004029 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4030 help='Sends your change to the CQ after an approval. Only '
4031 'works on repos that have the Auto-Submit label '
4032 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004033 parser.add_option('--parallel', action='store_true',
4034 help='Run all tests specified by input_api.RunTests in all '
4035 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004036 parser.add_option('--no-autocc', action='store_true',
4037 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004038 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004039 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004040 parser.add_option('-R', '--retry-failed', action='store_true',
4041 help='Retry failed tryjobs from old patchset immediately '
4042 'after uploading new patchset. Cannot be used with '
4043 '--use-commit-queue or --cq-dry-run.')
4044 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4045 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004046 parser.add_option('--fixed', '-x',
4047 help='List of bugs that will be commented on and marked '
4048 'fixed (pre-populates "Fixed:" tag). Same format as '
4049 '-b option / "Bug:" tag. If fixing several issues, '
4050 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004051 parser.add_option('--edit-description', action='store_true', default=False,
4052 help='Modify description before upload. Cannot be used '
4053 'with --force. It is a noop when --no-squash is set '
4054 'or a new commit is created.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004055
rmistry@google.com2dd99862015-06-22 12:22:18 +00004056 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004057 (options, args) = parser.parse_args(args)
4058
sbc@chromium.org71437c02015-04-09 19:29:40 +00004059 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004060 return 1
4061
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004062 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004063 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004064 options.cc = cleanup_list(options.cc)
4065
Josipe827b0f2020-01-30 00:07:20 +00004066 if options.edit_description and options.force:
4067 parser.error('Only one of --force and --edit-description allowed')
4068
tandriib80458a2016-06-23 12:20:07 -07004069 if options.message_file:
4070 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004071 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004072 options.message = gclient_utils.FileRead(options.message_file)
4073 options.message_file = None
4074
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004075 if ([options.cq_dry_run,
4076 options.use_commit_queue,
4077 options.retry_failed].count(True) > 1):
4078 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4079 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004080
Aaron Gableedbc4132017-09-11 13:22:28 -07004081 if options.use_commit_queue:
4082 options.send_mail = True
4083
Edward Lesmes0dd54822020-03-26 18:24:25 +00004084 if options.squash is None:
4085 # Load default for user, repo, squash=true, in this order.
4086 options.squash = settings.GetSquashGerritUploads()
4087
Edward Lemur934836a2019-09-09 20:16:54 +00004088 cl = Changelist()
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004089 # Warm change details cache now to avoid RPCs later, reducing latency for
4090 # developers.
4091 if cl.GetIssue():
4092 cl._GetChangeDetail(
4093 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4094
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004095 if options.retry_failed and not cl.GetIssue():
4096 print('No previous patchsets, so --retry-failed has no effect.')
4097 options.retry_failed = False
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004098
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004099 # cl.GetMostRecentPatchset uses cached information, and can return the last
4100 # patchset before upload. Calling it here makes it clear that it's the
4101 # last patchset before upload. Note that GetMostRecentPatchset will fail
4102 # if no CL has been uploaded yet.
4103 if options.retry_failed:
4104 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004105
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004106 ret = cl.CMDUpload(options, args, orig_args)
4107
4108 if options.retry_failed:
4109 if ret != 0:
4110 print('Upload failed, so --retry-failed has no effect.')
4111 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004112 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004113 cl, options.buildbucket_host, latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004114 jobs = _filter_failed_for_retry(builds)
4115 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004116 print('No failed tryjobs, so --retry-failed has no effect.')
4117 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004118 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004119
4120 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004121
4122
Francois Dorayd42c6812017-05-30 15:10:20 -04004123@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004124@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004125def CMDsplit(parser, args):
4126 """Splits a branch into smaller branches and uploads CLs.
4127
4128 Creates a branch and uploads a CL for each group of files modified in the
4129 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00004130 comment, the string '$directory', is replaced with the directory containing
4131 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04004132 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004133 parser.add_option('-d', '--description', dest='description_file',
4134 help='A text file containing a CL description in which '
4135 '$directory will be replaced by each CL\'s directory.')
4136 parser.add_option('-c', '--comment', dest='comment_file',
4137 help='A text file containing a CL comment.')
4138 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004139 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004140 help='List the files and reviewers for each CL that would '
4141 'be created, but don\'t create branches or CLs.')
4142 parser.add_option('--cq-dry-run', action='store_true',
4143 help='If set, will do a cq dry run for each uploaded CL. '
4144 'Please be careful when doing this; more than ~10 CLs '
4145 'has the potential to overload our build '
4146 'infrastructure. Try to upload these not during high '
4147 'load times (usually 11-3 Mountain View time). Email '
4148 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004149 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4150 default=True,
4151 help='Sends your change to the CQ after an approval. Only '
4152 'works on repos that have the Auto-Submit label '
4153 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004154 options, _ = parser.parse_args(args)
4155
4156 if not options.description_file:
4157 parser.error('No --description flag specified.')
4158
4159 def WrappedCMDupload(args):
4160 return CMDupload(OptionParser(), args)
4161
Edward Lemur2c62b332020-03-12 22:12:33 +00004162 return split_cl.SplitCl(
4163 options.description_file, options.comment_file, Changelist,
4164 WrappedCMDupload, options.dry_run, options.cq_dry_run,
4165 options.enable_auto_submit, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04004166
4167
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004168@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004169@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004170def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004171 """DEPRECATED: Used to commit the current changelist via git-svn."""
4172 message = ('git-cl no longer supports committing to SVN repositories via '
4173 'git-svn. You probably want to use `git cl land` instead.')
4174 print(message)
4175 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004176
4177
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004178@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004179@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004180def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004181 """Commits the current changelist via git.
4182
4183 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4184 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004185 """
4186 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4187 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004188 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004189 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004190 parser.add_option('--parallel', action='store_true',
4191 help='Run all tests specified by input_api.RunTests in all '
4192 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004193 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004194
Edward Lemur934836a2019-09-09 20:16:54 +00004195 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004196
Robert Iannucci2e73d432018-03-14 01:10:47 -07004197 if not cl.GetIssue():
4198 DieWithError('You must upload the change first to Gerrit.\n'
4199 ' If you would rather have `git cl land` upload '
4200 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004201 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004202 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203
4204
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004205@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004206@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004207def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004208 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004209 parser.add_option('-b', dest='newbranch',
4210 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004211 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004212 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004214 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004215
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004216 group = optparse.OptionGroup(
4217 parser,
4218 'Options for continuing work on the current issue uploaded from a '
4219 'different clone (e.g. different machine). Must be used independently '
4220 'from the other options. No issue number should be specified, and the '
4221 'branch must have an issue number associated with it')
4222 group.add_option('--reapply', action='store_true', dest='reapply',
4223 help='Reset the branch and reapply the issue.\n'
4224 'CAUTION: This will undo any local changes in this '
4225 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004226
4227 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004228 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004229 parser.add_option_group(group)
4230
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004232
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004233 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004234 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004235 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004236 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004237 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004238
Edward Lemur934836a2019-09-09 20:16:54 +00004239 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004240 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004241 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004242
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004243 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004244 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004245 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004246
4247 RunGit(['reset', '--hard', upstream])
4248 if options.pull:
4249 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004250
Edward Lemur678a6842019-10-03 22:25:05 +00004251 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4252 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004253
4254 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004255 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004256
Edward Lemurf38bc172019-09-03 21:02:13 +00004257 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004258 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004259 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004260
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004261 # We don't want uncommitted changes mixed up with the patch.
4262 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004263 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004265 if options.newbranch:
4266 if options.force:
4267 RunGit(['branch', '-D', options.newbranch],
4268 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00004269 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004270
Edward Lemur678a6842019-10-03 22:25:05 +00004271 cl = Changelist(
4272 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004273
Edward Lemur678a6842019-10-03 22:25:05 +00004274 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004275 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004276
Edward Lemurf38bc172019-09-03 21:02:13 +00004277 return cl.CMDPatchWithParsedIssue(
4278 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004279
4280
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004281def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004282 """Fetches the tree status and returns either 'open', 'closed',
4283 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004284 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285 if url:
Edward Lemur79d4f992019-11-11 23:49:02 +00004286 status = urllib.request.urlopen(url).read().lower()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287 if status.find('closed') != -1 or status == '0':
4288 return 'closed'
4289 elif status.find('open') != -1 or status == '1':
4290 return 'open'
4291 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004292 return 'unset'
4293
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295def GetTreeStatusReason():
4296 """Fetches the tree status from a json url and returns the message
4297 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004298 url = settings.GetTreeStatusUrl()
4299 json_url = urlparse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004300 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301 status = json.loads(connection.read())
4302 connection.close()
4303 return status['message']
4304
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004305
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004306@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004307def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004308 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004309 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004310 status = GetTreeStatus()
4311 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313 return 2
4314
vapiera7fbd5a2016-06-16 09:17:49 -07004315 print('The tree is %s' % status)
4316 print()
4317 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 if status != 'open':
4319 return 1
4320 return 0
4321
4322
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004323@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004324def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004325 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4326 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004327 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004328 '-b', '--bot', action='append',
4329 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4330 'times to specify multiple builders. ex: '
4331 '"-b win_rel -b win_layout". See '
4332 'the try server waterfall for the builders name and the tests '
4333 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004334 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004335 '-B', '--bucket', default='',
4336 help=('Buildbucket bucket to send the try requests.'))
4337 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004338 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004339 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004340 'be determined by the try recipe that builder runs, which usually '
4341 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004342 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004343 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004344 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004345 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004346 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004347 '--category', default='git_cl_try', help='Specify custom build category.')
4348 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004349 '--project',
4350 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004351 'in recipe to determine to which repository or directory to '
4352 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004353 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004354 '-p', '--property', dest='properties', action='append', default=[],
4355 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004356 'key2=value2 etc. The value will be treated as '
4357 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004358 'NOTE: using this may make your tryjob not usable for CQ, '
4359 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004360 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004361 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4362 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004363 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004364 parser.add_option(
4365 '-R', '--retry-failed', action='store_true', default=False,
4366 help='Retry failed jobs from the latest set of tryjobs. '
4367 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004368 parser.add_option(
4369 '-i', '--issue', type=int,
4370 help='Operate on this issue instead of the current branch\'s implicit '
4371 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004372 options, args = parser.parse_args(args)
4373
machenbach@chromium.org45453142015-09-15 08:45:22 +00004374 # Make sure that all properties are prop=value pairs.
4375 bad_params = [x for x in options.properties if '=' not in x]
4376 if bad_params:
4377 parser.error('Got properties with missing "=": %s' % bad_params)
4378
maruel@chromium.org15192402012-09-06 12:38:29 +00004379 if args:
4380 parser.error('Unknown arguments: %s' % args)
4381
Edward Lemur934836a2019-09-09 20:16:54 +00004382 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004383 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004384 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004385
Edward Lemurf38bc172019-09-03 21:02:13 +00004386 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004387 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004388
tandriie113dfd2016-10-11 10:20:12 -07004389 error_message = cl.CannotTriggerTryJobReason()
4390 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004391 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004392
Edward Lemur45768512020-03-02 19:03:14 +00004393 if options.bot:
4394 if options.retry_failed:
4395 parser.error('--bot is not compatible with --retry-failed.')
4396 if not options.bucket:
4397 parser.error('A bucket (e.g. "chromium/try") is required.')
4398
4399 triggered = [b for b in options.bot if 'triggered' in b]
4400 if triggered:
4401 parser.error(
4402 'Cannot schedule builds on triggered bots: %s.\n'
4403 'This type of bot requires an initial job from a parent (usually a '
4404 'builder). Schedule a job on the parent instead.\n' % triggered)
4405
4406 if options.bucket.startswith('.master'):
4407 parser.error('Buildbot masters are not supported.')
4408
4409 project, bucket = _parse_bucket(options.bucket)
4410 if project is None or bucket is None:
4411 parser.error('Invalid bucket: %s.' % options.bucket)
4412 jobs = sorted((project, bucket, bot) for bot in options.bot)
4413 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004414 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004415 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004416 if options.verbose:
4417 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00004418 jobs = _filter_failed_for_retry(builds)
4419 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004420 print('There are no failed jobs in the latest set of jobs '
4421 '(patchset #%d), doing nothing.' % patchset)
4422 return 0
Edward Lemur45768512020-03-02 19:03:14 +00004423 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004424 if num_builders > 10:
4425 confirm_or_exit('There are %d builders with failed builds.'
4426 % num_builders, action='continue')
4427 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07004428 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004429 print('git cl try with no bots now defaults to CQ dry run.')
4430 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4431 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004432
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004433 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004434 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004435 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004436 except BuildbucketResponseException as ex:
4437 print('ERROR: %s' % ex)
4438 return 1
4439 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004440
4441
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004442@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004443def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004444 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004445 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004446 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004447 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004448 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004449 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004450 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004451 '--color', action='store_true', default=setup_color.IS_TTY,
4452 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004453 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004454 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4455 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004456 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004457 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004458 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004459 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00004460 parser.add_option(
4461 '-i', '--issue', type=int,
4462 help='Operate on this issue instead of the current branch\'s implicit '
4463 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004464 options, args = parser.parse_args(args)
4465 if args:
4466 parser.error('Unrecognized args: %s' % ' '.join(args))
4467
Edward Lemur934836a2019-09-09 20:16:54 +00004468 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004469 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004470 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004471
tandrii221ab252016-10-06 08:12:04 -07004472 patchset = options.patchset
4473 if not patchset:
4474 patchset = cl.GetMostRecentPatchset()
4475 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004476 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004477 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004478 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004479 cl.GetIssue())
4480
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004481 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004482 jobs = _fetch_tryjobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004483 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004484 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004485 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004486 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004487 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004488 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004489 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004490 return 0
4491
4492
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004493@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004494@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004495def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004496 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004497 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004498 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004499 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004501 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004502 if args:
4503 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004504 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004505 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004506 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004507 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004508
4509 # Clear configured merge-base, if there is one.
4510 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004511 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004512 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004513 return 0
4514
4515
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004516@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004517def CMDweb(parser, args):
4518 """Opens the current CL in the web browser."""
4519 _, args = parser.parse_args(args)
4520 if args:
4521 parser.error('Unrecognized args: %s' % ' '.join(args))
4522
4523 issue_url = Changelist().GetIssueURL()
4524 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004525 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004526 return 1
4527
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004528 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004529 # allows us to hide the "Created new window in existing browser session."
4530 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004531 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004532 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004533 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004534 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004535 os.open(os.devnull, os.O_RDWR)
4536 try:
4537 webbrowser.open(issue_url)
4538 finally:
4539 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004540 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004541 return 0
4542
4543
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004544@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004545def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004546 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004547 parser.add_option('-d', '--dry-run', action='store_true',
4548 help='trigger in dry run mode')
4549 parser.add_option('-c', '--clear', action='store_true',
4550 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00004551 parser.add_option(
4552 '-i', '--issue', type=int,
4553 help='Operate on this issue instead of the current branch\'s implicit '
4554 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004555 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004556 if args:
4557 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004558 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004559 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004560
Edward Lemur934836a2019-09-09 20:16:54 +00004561 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004562 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004563 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004564 elif options.dry_run:
4565 state = _CQState.DRY_RUN
4566 else:
4567 state = _CQState.COMMIT
4568 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004569 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004570 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004571 return 0
4572
4573
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004574@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004575def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004576 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00004577 parser.add_option(
4578 '-i', '--issue', type=int,
4579 help='Operate on this issue instead of the current branch\'s implicit '
4580 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004581 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004582 if args:
4583 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004584 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004585 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004586 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004587 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004588 cl.CloseIssue()
4589 return 0
4590
4591
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004592@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004593def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004594 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004595 parser.add_option(
4596 '--stat',
4597 action='store_true',
4598 dest='stat',
4599 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004600 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004601 if args:
4602 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004603
Edward Lemur934836a2019-09-09 20:16:54 +00004604 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004605 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004606 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004607 if not issue:
4608 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004609
Aaron Gablea718c3e2017-08-28 17:47:28 -07004610 base = cl._GitGetBranchConfigValue('last-upload-hash')
4611 if not base:
4612 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4613 if not base:
4614 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4615 revision_info = detail['revisions'][detail['current_revision']]
4616 fetch_info = revision_info['fetch']['http']
4617 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4618 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004619
Aaron Gablea718c3e2017-08-28 17:47:28 -07004620 cmd = ['git', 'diff']
4621 if options.stat:
4622 cmd.append('--stat')
4623 cmd.append(base)
4624 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004625
4626 return 0
4627
4628
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004629@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004630def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004631 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004632 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004633 '--ignore-current',
4634 action='store_true',
4635 help='Ignore the CL\'s current reviewers and start from scratch.')
4636 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004637 '--ignore-self',
4638 action='store_true',
4639 help='Do not consider CL\'s author as an owners.')
4640 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004641 '--no-color',
4642 action='store_true',
4643 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07004644 parser.add_option(
4645 '--batch',
4646 action='store_true',
4647 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00004648 # TODO: Consider moving this to another command, since other
4649 # git-cl owners commands deal with owners for a given CL.
4650 parser.add_option(
4651 '--show-all',
4652 action='store_true',
4653 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004654 options, args = parser.parse_args(args)
4655
Edward Lemur934836a2019-09-09 20:16:54 +00004656 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00004657 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004658
Yang Guo6e269a02019-06-26 11:17:02 +00004659 if options.show_all:
4660 for arg in args:
4661 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemurb7f759f2020-03-04 21:20:56 +00004662 database = owners.Database(settings.GetRoot(), open, os.path)
Yang Guo6e269a02019-06-26 11:17:02 +00004663 database.load_data_needed_for([arg])
4664 print('Owners for %s:' % arg)
4665 for owner in sorted(database.all_possible_owners([arg], None)):
4666 print(' - %s' % owner)
4667 return 0
4668
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004669 if args:
4670 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004671 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004672 base_branch = args[0]
4673 else:
4674 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004675 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004676
Edward Lemur2c62b332020-03-12 22:12:33 +00004677 root = settings.GetRoot()
4678 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07004679
4680 if options.batch:
Edward Lemur2c62b332020-03-12 22:12:33 +00004681 db = owners.Database(root, open, os.path)
Dirk Prankebf980882017-09-02 15:08:00 -07004682 print('\n'.join(db.reviewers_for(affected_files, author)))
4683 return 0
4684
Edward Lemur2c62b332020-03-12 22:12:33 +00004685 owner_files = [f for f in affected_files if 'OWNERS' in os.path.basename(f)]
4686 original_owner_files = {
4687 f: scm.GIT.GetOldContents(root, f, base_branch).splitlines()
4688 for f in owner_files}
4689
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004690 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07004691 affected_files,
Edward Lemur2c62b332020-03-12 22:12:33 +00004692 root,
Edward Lemur707d70b2018-02-07 00:50:14 +01004693 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004694 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur2c62b332020-03-12 22:12:33 +00004695 fopen=open,
4696 os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02004697 disable_color=options.no_color,
Edward Lemur2c62b332020-03-12 22:12:33 +00004698 override_files=original_owner_files,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004699 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004700
4701
Aiden Bennerc08566e2018-10-03 17:52:42 +00004702def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004703 """Generates a diff command."""
4704 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00004705 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
4706
Aiden Benner6c18a1a2018-11-23 20:18:23 +00004707 if allow_prefix:
4708 # explicitly setting --src-prefix and --dst-prefix is necessary in the
4709 # case that diff.noprefix is set in the user's git config.
4710 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
4711 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00004712 diff_cmd += ['--no-prefix']
4713
4714 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004715
4716 if args:
4717 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004718 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004719 diff_cmd.append(arg)
4720 else:
4721 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004722
4723 return diff_cmd
4724
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004725
Jamie Madill5e96ad12020-01-13 16:08:35 +00004726def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
4727 """Runs clang-format-diff and sets a return value if necessary."""
4728
4729 if not clang_diff_files:
4730 return 0
4731
4732 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4733 # formatted. This is used to block during the presubmit.
4734 return_value = 0
4735
4736 # Locate the clang-format binary in the checkout
4737 try:
4738 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4739 except clang_format.NotFoundError as e:
4740 DieWithError(e)
4741
4742 if opts.full or settings.GetFormatFullByDefault():
4743 cmd = [clang_format_tool]
4744 if not opts.dry_run and not opts.diff:
4745 cmd.append('-i')
4746 if opts.dry_run:
4747 for diff_file in clang_diff_files:
4748 with open(diff_file, 'r') as myfile:
4749 code = myfile.read().replace('\r\n', '\n')
4750 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
4751 stdout = stdout.replace('\r\n', '\n')
4752 if opts.diff:
4753 sys.stdout.write(stdout)
4754 if code != stdout:
4755 return_value = 2
4756 else:
4757 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
4758 if opts.diff:
4759 sys.stdout.write(stdout)
4760 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00004761 try:
4762 script = clang_format.FindClangFormatScriptInChromiumTree(
4763 'clang-format-diff.py')
4764 except clang_format.NotFoundError as e:
4765 DieWithError(e)
4766
Edward Lesmes89624cd2020-04-06 17:51:56 +00004767 cmd = ['vpython', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00004768 if not opts.dry_run and not opts.diff:
4769 cmd.append('-i')
4770
4771 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00004772 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00004773
Edward Lesmes89624cd2020-04-06 17:51:56 +00004774 env = os.environ.copy()
4775 env['PATH'] = (
4776 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
4777 stdout = RunCommand(
4778 cmd, stdin=diff_output, cwd=top_dir, env=env,
4779 shell=bool(sys.platform.startswith('win32')))
Jamie Madill5e96ad12020-01-13 16:08:35 +00004780 if opts.diff:
4781 sys.stdout.write(stdout)
4782 if opts.dry_run and len(stdout) > 0:
4783 return_value = 2
4784
4785 return return_value
4786
4787
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004788def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004789 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004790 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004791
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004792
enne@chromium.org555cfe42014-01-29 18:21:39 +00004793@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004794@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004795def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004796 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11004797 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004798 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004799 parser.add_option('--full', action='store_true',
4800 help='Reformat the full content of all touched files')
4801 parser.add_option('--dry-run', action='store_true',
4802 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004803 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004804 '--no-clang-format',
4805 dest='clang_format',
4806 action='store_false',
4807 default=True,
4808 help='Disables formatting of various file types using clang-format.')
4809 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004810 '--python',
4811 action='store_true',
4812 default=None,
4813 help='Enables python formatting on all python files.')
4814 parser.add_option(
4815 '--no-python',
4816 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00004817 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004818 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00004819 'If neither --python or --no-python are set, python files that have a '
4820 '.style.yapf file in an ancestor directory will be formatted. '
4821 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004822 parser.add_option(
4823 '--js',
4824 action='store_true',
4825 help='Format javascript code with clang-format. '
4826 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004827 parser.add_option('--diff', action='store_true',
4828 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07004829 parser.add_option('--presubmit', action='store_true',
4830 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004831 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004832
Garrett Beaty91a6f332020-01-06 16:57:24 +00004833 if opts.python is not None and opts.no_python:
4834 raise parser.error('Cannot set both --python and --no-python')
4835 if opts.no_python:
4836 opts.python = False
4837
Daniel Chengc55eecf2016-12-30 03:11:02 -08004838 # Normalize any remaining args against the current path, so paths relative to
4839 # the current directory are still resolved as expected.
4840 args = [os.path.join(os.getcwd(), arg) for arg in args]
4841
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004842 # git diff generates paths against the root of the repository. Change
4843 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004844 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004845 if rel_base_path:
4846 os.chdir(rel_base_path)
4847
digit@chromium.org29e47272013-05-17 17:01:46 +00004848 # Grab the merge-base commit, i.e. the upstream commit of the current
4849 # branch when it was created or the last time it was rebased. This is
4850 # to cover the case where the user may have called "git fetch origin",
4851 # moving the origin branch to a newer commit, but hasn't rebased yet.
4852 upstream_commit = None
4853 cl = Changelist()
4854 upstream_branch = cl.GetUpstreamBranch()
4855 if upstream_branch:
4856 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4857 upstream_commit = upstream_commit.strip()
4858
4859 if not upstream_commit:
4860 DieWithError('Could not find base commit for this branch. '
4861 'Are you in detached state?')
4862
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004863 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4864 diff_output = RunGit(changed_files_cmd)
4865 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004866 # Filter out files deleted by this CL
4867 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004868
Andreas Haas417d89c2020-02-06 10:24:27 +00004869 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00004870 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11004871
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00004872 clang_diff_files = []
4873 if opts.clang_format:
4874 clang_diff_files = [
4875 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
4876 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004877 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4878 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004879 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004880
Edward Lesmes50da7702020-03-30 19:23:43 +00004881 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004882
Jamie Madill5e96ad12020-01-13 16:08:35 +00004883 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
4884 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004885
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004886 # Similar code to above, but using yapf on .py files rather than clang-format
4887 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004888 py_explicitly_disabled = opts.python is not None and not opts.python
4889 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00004890 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
4891 yapf_tool = os.path.join(depot_tools_path, 'yapf')
4892 if sys.platform.startswith('win'):
4893 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004894
Aiden Bennerc08566e2018-10-03 17:52:42 +00004895 # Used for caching.
4896 yapf_configs = {}
4897 for f in python_diff_files:
4898 # Find the yapf style config for the current file, defaults to depot
4899 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004900 _FindYapfConfigFile(f, yapf_configs, top_dir)
4901
4902 # Turn on python formatting by default if a yapf config is specified.
4903 # This breaks in the case of this repo though since the specified
4904 # style file is also the global default.
4905 if opts.python is None:
4906 filtered_py_files = []
4907 for f in python_diff_files:
4908 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
4909 filtered_py_files.append(f)
4910 else:
4911 filtered_py_files = python_diff_files
4912
4913 # Note: yapf still seems to fix indentation of the entire file
4914 # even if line ranges are specified.
4915 # See https://github.com/google/yapf/issues/499
4916 if not opts.full and filtered_py_files:
4917 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
4918
Brian Sheedyb4307d52019-12-02 19:18:17 +00004919 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
4920 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
4921 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00004922
Aiden Benner99b0ccb2018-11-20 19:53:31 +00004923 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00004924 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
4925 # Default to pep8 if not .style.yapf is found.
4926 if not yapf_style:
4927 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00004928
Andrew Grievefa40bfa2020-01-07 02:32:57 +00004929 cmd = [yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00004930
4931 has_formattable_lines = False
4932 if not opts.full:
4933 # Only run yapf over changed line ranges.
4934 for diff_start, diff_len in py_line_diffs[f]:
4935 diff_end = diff_start + diff_len - 1
4936 # Yapf errors out if diff_end < diff_start but this
4937 # is a valid line range diff for a removal.
4938 if diff_end >= diff_start:
4939 has_formattable_lines = True
4940 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
4941 # If all line diffs were removals we have nothing to format.
4942 if not has_formattable_lines:
4943 continue
4944
4945 if opts.diff or opts.dry_run:
4946 cmd += ['--diff']
4947 # Will return non-zero exit code if non-empty diff.
4948 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
4949 if opts.diff:
4950 sys.stdout.write(stdout)
4951 elif len(stdout) > 0:
4952 return_value = 2
4953 else:
4954 cmd += ['-i']
4955 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004956
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004957 # Dart's formatter does not have the nice property of only operating on
4958 # modified chunks, so hard code full.
4959 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004960 try:
4961 command = [dart_format.FindDartFmtToolInChromiumTree()]
4962 if not opts.dry_run and not opts.diff:
4963 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004964 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004965
ppi@chromium.org6593d932016-03-03 15:41:15 +00004966 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004967 if opts.dry_run and stdout:
4968 return_value = 2
Jamie Madill5e96ad12020-01-13 16:08:35 +00004969 except dart_format.NotFoundError:
vapiera7fbd5a2016-06-16 09:17:49 -07004970 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4971 'found in this checkout. Files in other languages are still '
4972 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004973
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004974 # Format GN build files. Always run on full build files for canonical form.
4975 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004976 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07004977 if opts.dry_run or opts.diff:
4978 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004979 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07004980 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Edward Lesmes89624cd2020-04-06 17:51:56 +00004981 shell=bool(sys.platform.startswith('win')),
brettw4b8ed592016-08-05 16:19:12 -07004982 cwd=top_dir)
4983 if opts.dry_run and gn_ret == 2:
4984 return_value = 2 # Not formatted.
4985 elif opts.diff and gn_ret == 2:
4986 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004987 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07004988 elif gn_ret != 0:
4989 # For non-dry run cases (and non-2 return values for dry-run), a
4990 # nonzero error code indicates a failure, probably because the file
4991 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004992 DieWithError('gn format failed on ' + gn_diff_file +
4993 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004994
Ilya Shermane081cbe2017-08-15 17:51:04 -07004995 # Skip the metrics formatting from the global presubmit hook. These files have
4996 # a separate presubmit hook that issues an error if the files need formatting,
4997 # whereas the top-level presubmit script merely issues a warning. Formatting
4998 # these files is somewhat slow, so it's important not to duplicate the work.
4999 if not opts.presubmit:
5000 for xml_dir in GetDirtyMetricsDirs(diff_files):
5001 tool_dir = os.path.join(top_dir, xml_dir)
5002 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5003 if opts.dry_run or opts.diff:
5004 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005005 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005006 if opts.diff:
5007 sys.stdout.write(stdout)
5008 if opts.dry_run and stdout:
5009 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005010
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005011 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005012
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005013
Steven Holte2e664bf2017-04-21 13:10:47 -07005014def GetDirtyMetricsDirs(diff_files):
5015 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5016 metrics_xml_dirs = [
5017 os.path.join('tools', 'metrics', 'actions'),
5018 os.path.join('tools', 'metrics', 'histograms'),
5019 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005020 os.path.join('tools', 'metrics', 'ukm'),
5021 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005022 for xml_dir in metrics_xml_dirs:
5023 if any(file.startswith(xml_dir) for file in xml_diff_files):
5024 yield xml_dir
5025
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005026
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005027@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005028@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005029def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005030 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005031 _, args = parser.parse_args(args)
5032
5033 if len(args) != 1:
5034 parser.print_help()
5035 return 1
5036
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005037 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005038 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005039 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005040
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005041 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005042
Edward Lemur52969c92020-02-06 18:15:28 +00005043 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00005044 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00005045 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005046
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005047 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005048 for key, issue in [x.split() for x in output.splitlines()]:
5049 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00005050 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00005051
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005052 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005053 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005054 return 1
5055 if len(branches) == 1:
5056 RunGit(['checkout', branches[0]])
5057 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005058 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005059 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005060 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00005061 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005062 try:
5063 RunGit(['checkout', branches[int(which)]])
5064 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005065 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005066 return 1
5067
5068 return 0
5069
5070
maruel@chromium.org29404b52014-09-08 22:58:00 +00005071def CMDlol(parser, args):
5072 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005073 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005074 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5075 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5076 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005077 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005078 return 0
5079
5080
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005081class OptionParser(optparse.OptionParser):
5082 """Creates the option parse and add --verbose support."""
5083 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005084 optparse.OptionParser.__init__(
5085 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005086 self.add_option(
5087 '-v', '--verbose', action='count', default=0,
5088 help='Use 2 times for more debugging info')
5089
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005090 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005091 try:
5092 return self._parse_args(args)
5093 finally:
5094 # Regardless of success or failure of args parsing, we want to report
5095 # metrics, but only after logging has been initialized (if parsing
5096 # succeeded).
5097 global settings
5098 settings = Settings()
5099
5100 if not metrics.DISABLE_METRICS_COLLECTION:
5101 # GetViewVCUrl ultimately calls logging method.
5102 project_url = settings.GetViewVCUrl().strip('/+')
5103 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5104 metrics.collector.add('project_urls', [project_url])
5105
5106 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005107 # Create an optparse.Values object that will store only the actual passed
5108 # options, without the defaults.
5109 actual_options = optparse.Values()
5110 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5111 # Create an optparse.Values object with the default options.
5112 options = optparse.Values(self.get_default_values().__dict__)
5113 # Update it with the options passed by the user.
5114 options._update_careful(actual_options.__dict__)
5115 # Store the options passed by the user in an _actual_options attribute.
5116 # We store only the keys, and not the values, since the values can contain
5117 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005118 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005119
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005120 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005121 logging.basicConfig(
5122 level=levels[min(options.verbose, len(levels) - 1)],
5123 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5124 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005125
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005126 return options, args
5127
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005129def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005130 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005131 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005132 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005133 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005134
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005135 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005136 dispatcher = subcommand.CommandDispatcher(__name__)
5137 try:
5138 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005139 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005140 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005141 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005142 if e.code != 500:
5143 raise
5144 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005145 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005146 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005147 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005148
5149
5150if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005151 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5152 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005153 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005154 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005155 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005156 sys.exit(main(sys.argv[1:]))