blob: a563fd95d33a956e788d49ec070b21e99d0ded24 [file] [log] [blame]
Edward Lemur1f3bafb2019-10-08 17:56:33 +00001#!/usr/bin/env vpython
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
thakis@chromium.org3421c992014-11-02 02:20:32 +000013import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000014import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010015import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000016import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000017import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010018import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010025import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000026import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070028import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000030import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000035from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000036import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000038import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000039import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000040import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000041import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000042import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000043import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000044import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000045import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000046import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000047import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000048import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000049import presubmit_support
50import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000051import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040052import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import watchlists
56
Edward Lemur79d4f992019-11-11 23:49:02 +000057from third_party import six
58from six.moves import urllib
59
60
61if sys.version_info.major == 3:
62 basestring = (str,) # pylint: disable=redefined-builtin
63
Edward Lemurb9830242019-10-30 22:19:20 +000064
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
Edward Lemur0f58ae42019-04-30 17:24:12 +000067# Traces for git push will be stored in a traces directory inside the
68# depot_tools checkout.
69DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
70TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000071PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000072
73# When collecting traces, Git hashes will be reduced to 6 characters to reduce
74# the size after compression.
75GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
76# Used to redact the cookies from the gitcookies file.
77GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
78
Edward Lemurd4d1ba42019-09-20 21:46:37 +000079MAX_ATTEMPTS = 3
80
Edward Lemur1b52d872019-05-09 21:12:12 +000081# The maximum number of traces we will keep. Multiplied by 3 since we store
82# 3 files per trace.
83MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000084# Message to be displayed to the user to inform where to find the traces for a
85# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000086TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000087'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000088'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000089' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000090'Copies of your gitcookies file and git config have been recorded at:\n'
91' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000092# Format of the message to be stored as part of the traces to give developers a
93# better context when they go through traces.
94TRACES_README_FORMAT = (
95'Date: %(now)s\n'
96'\n'
97'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
98'Title: %(title)s\n'
99'\n'
100'%(description)s\n'
101'\n'
102'Execution time: %(execution_time)s\n'
103'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000104
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800105POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000106DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000107REFS_THAT_ALIAS_TO_OTHER_REFS = {
108 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
109 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
110}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
thestig@chromium.org44202a22014-03-11 19:22:18 +0000112# Valid extensions for files we want to lint.
113DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
114DEFAULT_LINT_IGNORE_REGEX = r"$^"
115
Aiden Bennerc08566e2018-10-03 17:52:42 +0000116# File name for yapf style config files.
117YAPF_CONFIG_FILENAME = '.style.yapf'
118
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000119# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000120Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000121
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000122# Initialized in main()
123settings = None
124
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100125# Used by tests/git_cl_test.py to add extra logging.
126# Inside the weirdly failing test, add this:
127# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700128# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100129_IS_BEING_TESTED = False
130
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000131
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000132_KNOWN_GERRIT_TO_SHORT_URLS = {
133 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
134 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
135}
136
137
Christopher Lamf732cd52017-01-24 12:40:11 +1100138def DieWithError(message, change_desc=None):
139 if change_desc:
140 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000141 print('\n ** Content of CL description **\n' +
142 '='*72 + '\n' +
143 change_desc.description + '\n' +
144 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100145
vapiera7fbd5a2016-06-16 09:17:49 -0700146 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000147 sys.exit(1)
148
149
Christopher Lamf732cd52017-01-24 12:40:11 +1100150def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000151 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000152 print('\nsaving CL description to %s\n' % backup_path)
Josip906bfde2020-01-31 22:38:49 +0000153 with open(backup_path, 'w') as backup_file:
154 backup_file.write(change_desc.description)
Christopher Lamf732cd52017-01-24 12:40:11 +1100155
156
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000157def GetNoGitPagerEnv():
158 env = os.environ.copy()
159 # 'cat' is a magical git string that disables pagers on all platforms.
160 env['GIT_PAGER'] = 'cat'
161 return env
162
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000163
bsep@chromium.org627d9002016-04-29 00:00:52 +0000164def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000165 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000166 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
167 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000168 except subprocess2.CalledProcessError as e:
169 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000170 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000172 'Command "%s" failed.\n%s' % (
173 ' '.join(args), error_message or e.stdout or ''))
Edward Lemur79d4f992019-11-11 23:49:02 +0000174 return e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000175
176
177def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000178 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000179 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000180
181
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000182def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000183 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700184 if suppress_stderr:
185 stderr = subprocess2.VOID
186 else:
187 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000188 try:
tandrii5d48c322016-08-18 16:19:37 -0700189 (out, _), code = subprocess2.communicate(['git'] + args,
190 env=GetNoGitPagerEnv(),
191 stdout=subprocess2.PIPE,
192 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000193 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700194 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900195 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000196 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000197
198
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000199def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000200 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000201 return RunGitWithCode(args, suppress_stderr=True)[1]
202
203
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000204def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000205 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000206 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000207 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000208 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000209
210
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000211def BranchExists(branch):
212 """Return True if specified branch exists."""
213 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
214 suppress_stderr=True)
215 return not code
216
217
tandrii2a16b952016-10-19 07:09:44 -0700218def time_sleep(seconds):
219 # Use this so that it can be mocked in tests without interfering with python
220 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700221 return time.sleep(seconds)
222
223
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000224def time_time():
225 # Use this so that it can be mocked in tests without interfering with python
226 # system machinery.
227 return time.time()
228
229
Edward Lemur1b52d872019-05-09 21:12:12 +0000230def datetime_now():
231 # Use this so that it can be mocked in tests without interfering with python
232 # system machinery.
233 return datetime.datetime.now()
234
235
Edward Lemur1a83da12020-03-04 21:18:36 +0000236def _raw_input(message):
237 # Use this so that it can be mocked in tests on Python 2 and 3.
238 if sys.version_info.major == 2:
239 return raw_input(message)
240 return input(message)
241
242
maruel@chromium.org90541732011-04-01 17:54:18 +0000243def ask_for_data(prompt):
244 try:
Edward Lemur1a83da12020-03-04 21:18:36 +0000245 return _raw_input(prompt)
maruel@chromium.org90541732011-04-01 17:54:18 +0000246 except KeyboardInterrupt:
247 # Hide the exception.
248 sys.exit(1)
249
250
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100251def confirm_or_exit(prefix='', action='confirm'):
252 """Asks user to press enter to continue or press Ctrl+C to abort."""
253 if not prefix or prefix.endswith('\n'):
254 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100255 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100256 mid = ' Press'
257 elif prefix.endswith(' '):
258 mid = 'press'
259 else:
260 mid = ' press'
261 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
262
263
264def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000265 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100266 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
267 while True:
268 if 'yes'.startswith(result):
269 return True
270 if 'no'.startswith(result):
271 return False
272 result = ask_for_data('Please, type yes or no: ').lower()
273
274
tandrii5d48c322016-08-18 16:19:37 -0700275def _git_branch_config_key(branch, key):
276 """Helper method to return Git config key for a branch."""
277 assert branch, 'branch name is required to set git config for it'
278 return 'branch.%s.%s' % (branch, key)
279
280
machenbach@chromium.org45453142015-09-15 08:45:22 +0000281def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000282 prop_list = getattr(options, 'properties', [])
283 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000284 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000285 try:
286 properties[key] = json.loads(val)
287 except ValueError:
288 pass # If a value couldn't be evaluated, treat it as a string.
289 return properties
290
291
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000292# TODO(crbug.com/976104): Remove this function once git-cl try-results has
293# migrated to use buildbucket v2
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000294def _buildbucket_retry(operation_name, http, *args, **kwargs):
295 """Retries requests to buildbucket service and returns parsed json content."""
296 try_count = 0
297 while True:
298 response, content = http.request(*args, **kwargs)
299 try:
300 content_json = json.loads(content)
301 except ValueError:
302 content_json = None
303
304 # Buildbucket could return an error even if status==200.
305 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000306 error = content_json.get('error')
307 if error.get('code') == 403:
308 raise BuildbucketResponseException(
309 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000310 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000311 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000312 raise BuildbucketResponseException(msg)
313
314 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700315 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000316 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000317 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700318 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000319 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000320 content)
321 return content_json
322 if response.status < 500 or try_count >= 2:
323 raise httplib2.HttpLib2Error(content)
324
325 # status >= 500 means transient failures.
326 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000327 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000328 try_count += 1
329 assert False, 'unreachable'
330
331
Edward Lemur4c707a22019-09-24 21:13:43 +0000332def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000333 """Calls a buildbucket v2 method and returns the parsed json response."""
334 headers = {
335 'Accept': 'application/json',
336 'Content-Type': 'application/json',
337 }
338 request = json.dumps(request)
339 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
340
341 logging.info('POST %s with %s' % (url, request))
342
343 attempts = 1
344 time_to_sleep = 1
345 while True:
346 response, content = http.request(url, 'POST', body=request, headers=headers)
347 if response.status == 200:
348 return json.loads(content[4:])
349 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
350 msg = '%s error when calling POST %s with %s: %s' % (
351 response.status, url, request, content)
352 raise BuildbucketResponseException(msg)
353 logging.debug(
354 '%s error when calling POST %s with %s. '
355 'Sleeping for %d seconds and retrying...' % (
356 response.status, url, request, time_to_sleep))
357 time.sleep(time_to_sleep)
358 time_to_sleep *= 2
359 attempts += 1
360
361 assert False, 'unreachable'
362
363
Edward Lemur6215c792019-10-03 21:59:05 +0000364def _parse_bucket(raw_bucket):
365 legacy = True
366 project = bucket = None
367 if '/' in raw_bucket:
368 legacy = False
369 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000370 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000371 elif raw_bucket.startswith('luci.'):
372 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000373 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000374 elif '.' in raw_bucket:
375 project = raw_bucket.split('.')[0]
376 bucket = raw_bucket
377 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000378 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000379 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
380 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000381
382
Quinten Yearsley777660f2020-03-04 23:37:06 +0000383def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000384 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700385
386 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000387 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000388 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 options: Command-line options.
390 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000391 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000392 for project, bucket, builder in jobs:
393 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000394 print('To see results here, run: git cl try-results')
395 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700396
Quinten Yearsley777660f2020-03-04 23:37:06 +0000397 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000398 if not requests:
399 return
400
Edward Lemur5b929a42019-10-21 17:57:39 +0000401 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000402 http.force_exception_to_status_code = True
403
404 batch_request = {'requests': requests}
405 batch_response = _call_buildbucket(
406 http, options.buildbucket_host, 'Batch', batch_request)
407
408 errors = [
409 ' ' + response['error']['message']
410 for response in batch_response.get('responses', [])
411 if 'error' in response
412 ]
413 if errors:
414 raise BuildbucketResponseException(
415 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
416
417
Quinten Yearsley777660f2020-03-04 23:37:06 +0000418def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000419 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000420 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000421 shared_properties = {
422 'category': options.ensure_value('category', 'git_cl_try')
423 }
424 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000425 shared_properties['clobber'] = True
426 shared_properties.update(_get_properties_from_options(options) or {})
427
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000428 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000429 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000430 shared_tags.append({'key': 'retry_failed',
431 'value': '1'})
432
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000433 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000434 for (project, bucket, builder) in jobs:
435 properties = shared_properties.copy()
436 if 'presubmit' in builder.lower():
437 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000438
Edward Lemur45768512020-03-02 19:03:14 +0000439 requests.append({
440 'scheduleBuild': {
441 'requestId': str(uuid.uuid4()),
442 'builder': {
443 'project': getattr(options, 'project', None) or project,
444 'bucket': bucket,
445 'builder': builder,
446 },
447 'gerritChanges': gerrit_changes,
448 'properties': properties,
449 'tags': [
450 {'key': 'builder', 'value': builder},
451 ] + shared_tags,
452 }
453 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000454
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000455 if options.ensure_value('revision', None):
Edward Lemur45768512020-03-02 19:03:14 +0000456 requests[-1]['scheduleBuild']['gitilesCommit'] = {
457 'host': gerrit_changes[0]['host'],
458 'project': gerrit_changes[0]['project'],
459 'id': options.revision
460 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000461
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000462 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000463
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000464
Quinten Yearsley777660f2020-03-04 23:37:06 +0000465def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000466 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000467
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000468 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000469 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000470 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000471 request = {
472 'predicate': {
473 'gerritChanges': [changelist.GetGerritChange(patchset)],
474 },
475 'fields': ','.join('builds.*.' + field for field in fields),
476 }
tandrii221ab252016-10-06 08:12:04 -0700477
Edward Lemur5b929a42019-10-21 17:57:39 +0000478 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000479 if authenticator.has_cached_credentials():
480 http = authenticator.authorize(httplib2.Http())
481 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700482 print('Warning: Some results might be missing because %s' %
483 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000484 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000485 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486 http.force_exception_to_status_code = True
487
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000488 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
489 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000490
Edward Lemur45768512020-03-02 19:03:14 +0000491
Edward Lemur5b929a42019-10-21 17:57:39 +0000492def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000493 """Fetches builds from the latest patchset that has builds (within
494 the last few patchsets).
495
496 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000497 changelist (Changelist): The CL to fetch builds for
498 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000499 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
500 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000501 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000502 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
503 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000504 """
505 assert buildbucket_host
506 assert changelist.GetIssue(), 'CL must be uploaded first'
507 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000508 if latest_patchset is None:
509 assert changelist.GetMostRecentPatchset()
510 ps = changelist.GetMostRecentPatchset()
511 else:
512 assert latest_patchset > 0, latest_patchset
513 ps = latest_patchset
514
Quinten Yearsley983111f2019-09-26 17:18:48 +0000515 min_ps = max(1, ps - 5)
516 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000517 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000518 if len(builds):
519 return builds, ps
520 ps -= 1
521 return [], 0
522
523
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000524def _filter_failed_for_retry(all_builds):
525 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000526
527 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000528 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000529 i.e. a list of buildbucket.v2.Builds which includes status and builder
530 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000531
532 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000533 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000534 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000535 """
Edward Lemur45768512020-03-02 19:03:14 +0000536 grouped = {}
537 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000538 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000539 key = (builder['project'], builder['bucket'], builder['builder'])
540 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000541
Edward Lemur45768512020-03-02 19:03:14 +0000542 jobs = []
543 for (project, bucket, builder), builds in grouped.items():
544 if 'triggered' in builder:
545 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
546 'from a parent. Please schedule a manual job for the parent '
547 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000548 continue
549 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
550 # Don't retry if any are running.
551 continue
Edward Lemur45768512020-03-02 19:03:14 +0000552 # If builder had several builds, retry only if the last one failed.
553 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
554 # build, but in case of retrying failed jobs retrying a flaky one makes
555 # sense.
556 builds = sorted(builds, key=lambda b: b['createTime'])
557 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
558 continue
559 # Don't retry experimental build previously triggered by CQ.
560 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
561 for t in builds[-1]['tags']):
562 continue
563 jobs.append((project, bucket, builder))
564
565 # Sort the jobs to make testing easier.
566 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000567
568
Quinten Yearsley777660f2020-03-04 23:37:06 +0000569def _print_tryjobs(options, builds):
570 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000572 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 return
574
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000575 longest_builder = max(len(b['builder']['builder']) for b in builds)
576 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000578 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
579 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000581 builds_by_status = {}
582 for b in builds:
583 builds_by_status.setdefault(b['status'], []).append({
584 'id': b['id'],
585 'name': name_fmt.format(
586 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
587 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000589 sort_key = lambda b: (b['name'], b['id'])
590
591 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000593 if not builds:
594 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000596 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000597 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000598 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 else:
600 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
601
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000602 print(colorize(title))
603 for b in sorted(builds, key=sort_key):
604 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000605
606 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000607 print_builds(
608 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
609 print_builds(
610 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
611 color=Fore.MAGENTA)
612 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
613 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
614 color=Fore.MAGENTA)
615 print_builds('Started:', builds_by_status.pop('STARTED', []))
616 print_builds(
617 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000619 print_builds(
620 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000621 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000622
623
Aiden Bennerc08566e2018-10-03 17:52:42 +0000624def _ComputeDiffLineRanges(files, upstream_commit):
625 """Gets the changed line ranges for each file since upstream_commit.
626
627 Parses a git diff on provided files and returns a dict that maps a file name
628 to an ordered list of range tuples in the form (start_line, count).
629 Ranges are in the same format as a git diff.
630 """
631 # If files is empty then diff_output will be a full diff.
632 if len(files) == 0:
633 return {}
634
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000635 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000636 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000637 diff_output = RunGit(diff_cmd)
638
639 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
640 # 2 capture groups
641 # 0 == fname of diff file
642 # 1 == 'diff_start,diff_count' or 'diff_start'
643 # will match each of
644 # diff --git a/foo.foo b/foo.py
645 # @@ -12,2 +14,3 @@
646 # @@ -12,2 +17 @@
647 # running re.findall on the above string with pattern will give
648 # [('foo.py', ''), ('', '14,3'), ('', '17')]
649
650 curr_file = None
651 line_diffs = {}
652 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
653 if match[0] != '':
654 # Will match the second filename in diff --git a/a.py b/b.py.
655 curr_file = match[0]
656 line_diffs[curr_file] = []
657 else:
658 # Matches +14,3
659 if ',' in match[1]:
660 diff_start, diff_count = match[1].split(',')
661 else:
662 # Single line changes are of the form +12 instead of +12,1.
663 diff_start = match[1]
664 diff_count = 1
665
666 diff_start = int(diff_start)
667 diff_count = int(diff_count)
668
669 # If diff_count == 0 this is a removal we can ignore.
670 line_diffs[curr_file].append((diff_start, diff_count))
671
672 return line_diffs
673
674
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000675def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000676 """Checks if a yapf file is in any parent directory of fpath until top_dir.
677
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000678 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000679 is found returns None. Uses yapf_config_cache as a cache for previously found
680 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000681 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000682 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000683 # Return result if we've already computed it.
684 if fpath in yapf_config_cache:
685 return yapf_config_cache[fpath]
686
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000687 parent_dir = os.path.dirname(fpath)
688 if os.path.isfile(fpath):
689 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000690 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000691 # Otherwise fpath is a directory
692 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
693 if os.path.isfile(yapf_file):
694 ret = yapf_file
695 elif fpath == top_dir or parent_dir == fpath:
696 # If we're at the top level directory, or if we're at root
697 # there is no provided style.
698 ret = None
699 else:
700 # Otherwise recurse on the current directory.
701 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000702 yapf_config_cache[fpath] = ret
703 return ret
704
705
Brian Sheedyb4307d52019-12-02 19:18:17 +0000706def _GetYapfIgnorePatterns(top_dir):
707 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000708
709 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
710 but this functionality appears to break when explicitly passing files to
711 yapf for formatting. According to
712 https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
713 the .yapfignore file should be in the directory that yapf is invoked from,
714 which we assume to be the top level directory in this case.
715
716 Args:
717 top_dir: The top level directory for the repository being formatted.
718
719 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000720 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000721 """
722 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000723 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000724 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000725 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000726
Brian Sheedyb4307d52019-12-02 19:18:17 +0000727 with open(yapfignore_file) as f:
728 for line in f.readlines():
729 stripped_line = line.strip()
730 # Comments and blank lines should be ignored.
731 if stripped_line.startswith('#') or stripped_line == '':
732 continue
733 ignore_patterns.add(stripped_line)
734 return ignore_patterns
735
736
737def _FilterYapfIgnoredFiles(filepaths, patterns):
738 """Filters out any filepaths that match any of the given patterns.
739
740 Args:
741 filepaths: An iterable of strings containing filepaths to filter.
742 patterns: An iterable of strings containing fnmatch patterns to filter on.
743
744 Returns:
745 A list of strings containing all the elements of |filepaths| that did not
746 match any of the patterns in |patterns|.
747 """
748 # Not inlined so that tests can use the same implementation.
749 return [f for f in filepaths
750 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000751
752
Aaron Gable13101a62018-02-09 13:20:41 -0800753def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754 """Prints statistics about the change to the user."""
755 # --no-ext-diff is broken in some versions of Git, so try to work around
756 # this by overriding the environment (but there is still a problem if the
757 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000759 if 'GIT_EXTERNAL_DIFF' in env:
760 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000761
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000762 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800763 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000764 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000765
766
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000767class BuildbucketResponseException(Exception):
768 pass
769
770
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771class Settings(object):
772 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000774 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 self.tree_status_url = None
776 self.viewvc_url = None
777 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000778 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000779 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000780 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000781 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000782 self.format_full_by_default = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
Edward Lemur26964072020-02-19 19:18:51 +0000784 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000786 if self.updated:
787 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000788
Edward Lemur26964072020-02-19 19:18:51 +0000789 # The only value that actually changes the behavior is
790 # autoupdate = "false". Everything else means "true".
791 autoupdate = (
792 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
793
794 cr_settings_file = FindCodereviewSettingsFile()
795 if autoupdate != 'false' and cr_settings_file:
796 LoadCodereviewSettingsFromFile(cr_settings_file)
797 cr_settings_file.close()
798
799 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000801 @staticmethod
802 def GetRelativeRoot():
803 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000806 if self.root is None:
807 self.root = os.path.abspath(self.GetRelativeRoot())
808 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def GetTreeStatusUrl(self, error_ok=False):
811 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000812 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
813 if self.tree_status_url is None and not error_ok:
814 DieWithError(
815 'You must configure your tree status URL by running '
816 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 return self.tree_status_url
818
819 def GetViewVCUrl(self):
820 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000821 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 return self.viewvc_url
823
rmistry@google.com90752582014-01-14 21:04:50 +0000824 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000825 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000826
rmistry@google.com5626a922015-02-26 14:03:30 +0000827 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000828 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000829 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000830 return run_post_upload_hook == "True"
831
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000832 def GetDefaultCCList(self):
Edward Lemur26964072020-02-19 19:18:51 +0000833 return self._GetConfig('rietveld.cc')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000834
ukai@chromium.orge8077812012-02-03 03:41:46 +0000835 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000836 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000837 if self.is_gerrit is None:
Edward Lemur26964072020-02-19 19:18:51 +0000838 self.is_gerrit = self._GetConfig('gerrit.host').lower() == 'true'
ukai@chromium.orge8077812012-02-03 03:41:46 +0000839 return self.is_gerrit
840
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000841 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000842 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000843 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700844 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
Edward Lemur26964072020-02-19 19:18:51 +0000845 if self.squash_gerrit_uploads is None:
846 # Default is squash now (http://crbug.com/611892#c23).
847 self.squash_gerrit_uploads = self._GetConfig(
848 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000849 return self.squash_gerrit_uploads
850
tandriia60502f2016-06-20 02:01:53 -0700851 def GetSquashGerritUploadsOverride(self):
852 """Return True or False if codereview.settings should be overridden.
853
854 Returns None if no override has been defined.
855 """
856 # See also http://crbug.com/611892#c23
Edward Lemur26964072020-02-19 19:18:51 +0000857 result = self._GetConfig('gerrit.override-squash-uploads').lower()
tandriia60502f2016-06-20 02:01:53 -0700858 if result == 'true':
859 return True
860 if result == 'false':
861 return False
862 return None
863
tandrii@chromium.org28253532016-04-14 13:46:56 +0000864 def GetGerritSkipEnsureAuthenticated(self):
865 """Return True if EnsureAuthenticated should not be done for Gerrit
866 uploads."""
867 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000868 self.gerrit_skip_ensure_authenticated = self._GetConfig(
869 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000870 return self.gerrit_skip_ensure_authenticated
871
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000872 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000873 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000874 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000875 # Git requires single quotes for paths with spaces. We need to replace
876 # them with double quotes for Windows to treat such paths as a single
877 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000878 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000879 return self.git_editor or None
880
thestig@chromium.org44202a22014-03-11 19:22:18 +0000881 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000882 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000883
884 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000885 return self._GetConfig(
886 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000887
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000888 def GetFormatFullByDefault(self):
889 if self.format_full_by_default is None:
890 result = (
891 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
892 error_ok=True).strip())
893 self.format_full_by_default = (result == 'true')
894 return self.format_full_by_default
895
Edward Lemur26964072020-02-19 19:18:51 +0000896 def _GetConfig(self, key, default=''):
897 self._LazyUpdateIfNeeded()
898 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899
900
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000901class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000902 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000903 NONE = 'none'
904 DRY_RUN = 'dry_run'
905 COMMIT = 'commit'
906
907 ALL_STATES = [NONE, DRY_RUN, COMMIT]
908
909
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000910class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000911 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000912 self.issue = issue
913 self.patchset = patchset
914 self.hostname = hostname
915
916 @property
917 def valid(self):
918 return self.issue is not None
919
920
Edward Lemurf38bc172019-09-03 21:02:13 +0000921def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000922 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
923 fail_result = _ParsedIssueNumberArgument()
924
Edward Lemur678a6842019-10-03 22:25:05 +0000925 if isinstance(arg, int):
926 return _ParsedIssueNumberArgument(issue=arg)
927 if not isinstance(arg, basestring):
928 return fail_result
929
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000930 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000931 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000932 if not arg.startswith('http'):
933 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700934
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000935 url = gclient_utils.UpgradeToHttps(arg)
936 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000937 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000938 except ValueError:
939 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200940
Edward Lemur678a6842019-10-03 22:25:05 +0000941 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
942 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
943 # Short urls like https://domain/<issue_number> can be used, but don't allow
944 # specifying the patchset (you'd 404), but we allow that here.
945 if parsed_url.path == '/':
946 part = parsed_url.fragment
947 else:
948 part = parsed_url.path
949
950 match = re.match(
951 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
952 if not match:
953 return fail_result
954
955 issue = int(match.group('issue'))
956 patchset = match.group('patchset')
957 return _ParsedIssueNumberArgument(
958 issue=issue,
959 patchset=int(patchset) if patchset else None,
960 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000961
962
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000963def _create_description_from_log(args):
964 """Pulls out the commit log to use as a base for the CL description."""
965 log_args = []
966 if len(args) == 1 and not args[0].endswith('.'):
967 log_args = [args[0] + '..']
968 elif len(args) == 1 and args[0].endswith('...'):
969 log_args = [args[0][:-1]]
970 elif len(args) == 2:
971 log_args = [args[0] + '..' + args[1]]
972 else:
973 log_args = args[:] # Hope for the best!
Edward Lemura12175c2020-03-09 16:58:26 +0000974 return RunGit(['log', '--pretty=format:%s%n%n%b'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000975
976
Aaron Gablea45ee112016-11-22 15:14:38 -0800977class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700978 def __init__(self, issue, url):
979 self.issue = issue
980 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800981 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700982
983 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800984 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700985 self.issue, self.url)
986
987
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100988_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +0000989 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +0100990 # TODO(tandrii): these two aren't known in Gerrit.
991 'approval', 'disapproval'])
992
993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000994class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000995 """Changelist works with one changelist in local branch.
996
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000997 Notes:
998 * Not safe for concurrent multi-{thread,process} use.
999 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001000 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001001 """
1002
Edward Lemur125d60a2019-09-13 18:25:41 +00001003 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001004 """Create a new ChangeList instance.
1005
Edward Lemurf38bc172019-09-03 21:02:13 +00001006 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001009 global settings
1010 if not settings:
1011 # Happens when git_cl.py is used as a utility library.
1012 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001013
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 self.branchref = branchref
1015 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001016 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +00001017 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
1019 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001021 self.lookedup_issue = False
1022 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001024 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001026 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001027 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001028 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001029 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001030
Edward Lemur125d60a2019-09-13 18:25:41 +00001031 # Lazily cached values.
1032 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1033 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1034 # Map from change number (issue) to its detail cache.
1035 self._detail_cache = {}
1036
1037 if codereview_host is not None:
1038 assert not codereview_host.startswith('https://'), codereview_host
1039 self._gerrit_host = codereview_host
1040 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001041
1042 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001043 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001044
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001045 The return value is a string suitable for passing to git cl with the --cc
1046 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001047 """
1048 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001049 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001050 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001051 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1052 return self.cc
1053
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001054 def GetCCListWithoutDefault(self):
1055 """Return the users cc'd on this CL excluding default ones."""
1056 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001057 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001058 return self.cc
1059
Daniel Cheng7227d212017-11-17 08:12:37 -08001060 def ExtendCC(self, more_cc):
1061 """Extends the list of users to cc on this CL based on the changed files."""
1062 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063
1064 def GetBranch(self):
1065 """Returns the short branch name, e.g. 'master'."""
1066 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +00001067 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001068 if not branchref:
1069 return None
1070 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +00001071 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 return self.branch
1073
1074 def GetBranchRef(self):
1075 """Returns the full branch name, e.g. 'refs/heads/master'."""
1076 self.GetBranch() # Poke the lazy loader.
1077 return self.branchref
1078
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001079 def ClearBranch(self):
1080 """Clears cached branch data of this object."""
1081 self.branch = self.branchref = None
1082
Edward Lemur85153282020-02-14 22:06:29 +00001083 def _GitGetBranchConfigValue(self, key, default=None):
1084 return scm.GIT.GetBranchConfig(
1085 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001086
Edward Lemur85153282020-02-14 22:06:29 +00001087 def _GitSetBranchConfigValue(self, key, value):
1088 action = 'set %s to %r' % (key, value)
1089 if not value:
1090 action = 'unset %s' % key
1091 assert self.GetBranch(), 'a branch is needed to ' + action
1092 return scm.GIT.SetBranchConfig(
1093 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 @staticmethod
1096 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001097 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 e.g. 'origin', 'refs/heads/master'
1099 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001100 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1101 settings.GetRoot(), branch)
1102 if not remote or not upstream_branch:
1103 DieWithError(
1104 'Unable to determine default branch to diff against.\n'
1105 'Either pass complete "git diff"-style arguments, like\n'
1106 ' git cl upload origin/master\n'
1107 'or verify this branch is set up to track another \n'
1108 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
1110 return remote, upstream_branch
1111
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001112 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001113 upstream_branch = self.GetUpstreamBranch()
1114 if not BranchExists(upstream_branch):
1115 DieWithError('The upstream for the current branch (%s) does not exist '
1116 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001117 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001118 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 def GetUpstreamBranch(self):
1121 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001122 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001123 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001124 upstream_branch = upstream_branch.replace('refs/heads/',
1125 'refs/remotes/%s/' % remote)
1126 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1127 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.upstream_branch = upstream_branch
1129 return self.upstream_branch
1130
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001131 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001132 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001133 remote, branch = None, self.GetBranch()
1134 seen_branches = set()
1135 while branch not in seen_branches:
1136 seen_branches.add(branch)
1137 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001138 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001139 if remote != '.' or branch.startswith('refs/remotes'):
1140 break
1141 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001142 remotes = RunGit(['remote'], error_ok=True).split()
1143 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001144 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001145 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001146 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001147 logging.warn('Could not determine which remote this change is '
1148 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001149 else:
1150 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001151 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001152 branch = 'HEAD'
1153 if branch.startswith('refs/remotes'):
1154 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001155 elif branch.startswith('refs/branch-heads/'):
1156 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001157 else:
1158 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001159 return self._remote
1160
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 def GitSanityChecks(self, upstream_git_obj):
1162 """Checks git repo status and ensures diff is from local commits."""
1163
sbc@chromium.org79706062015-01-14 21:18:12 +00001164 if upstream_git_obj is None:
1165 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001166 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001167 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001168 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001169 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001170 return False
1171
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001172 # Verify the commit we're diffing against is in our current branch.
1173 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1174 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1175 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001176 print('ERROR: %s is not in the current branch. You may need to rebase '
1177 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 return False
1179
1180 # List the commits inside the diff, and verify they are all local.
1181 commits_in_diff = RunGit(
1182 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1183 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1184 remote_branch = remote_branch.strip()
1185 if code != 0:
1186 _, remote_branch = self.GetRemoteBranch()
1187
1188 commits_in_remote = RunGit(
1189 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1190
1191 common_commits = set(commits_in_diff) & set(commits_in_remote)
1192 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001193 print('ERROR: Your diff contains %d commits already in %s.\n'
1194 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1195 'the diff. If you are using a custom git flow, you can override'
1196 ' the reference used for this check with "git config '
1197 'gitcl.remotebranch <git-ref>".' % (
1198 len(common_commits), remote_branch, upstream_git_obj),
1199 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001200 return False
1201 return True
1202
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001203 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001204 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001205
1206 Returns None if it is not set.
1207 """
tandrii5d48c322016-08-18 16:19:37 -07001208 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001209
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 def GetRemoteUrl(self):
1211 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1212
1213 Returns None if there is no remote.
1214 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001215 is_cached, value = self._cached_remote_url
1216 if is_cached:
1217 return value
1218
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001219 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001220 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001221
Edward Lemur298f2cf2019-02-22 21:40:39 +00001222 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001223 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001224 if host:
1225 self._cached_remote_url = (True, url)
1226 return url
1227
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001228 # If it cannot be parsed as an url, assume it is a local directory,
1229 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001230 logging.warning('"%s" doesn\'t appear to point to a git host. '
1231 'Interpreting it as a local directory.', url)
1232 if not os.path.isdir(url):
1233 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001234 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1235 'but it doesn\'t exist.',
1236 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001237 return None
1238
1239 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001240 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001241
Edward Lemur79d4f992019-11-11 23:49:02 +00001242 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001243 if not host:
1244 logging.error(
1245 'Remote "%(remote)s" for branch "%(branch)s" points to '
1246 '"%(cache_path)s", but it is misconfigured.\n'
1247 '"%(cache_path)s" must be a git repo and must have a remote named '
1248 '"%(remote)s" pointing to the git host.', {
1249 'remote': remote,
1250 'cache_path': cache_path,
1251 'branch': self.GetBranch()})
1252 return None
1253
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001254 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001255 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001257 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001258 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001259 if self.issue is None and not self.lookedup_issue:
Edward Lemur85153282020-02-14 22:06:29 +00001260 self.issue = self._GitGetBranchConfigValue(self.IssueConfigKey())
1261 if self.issue is not None:
1262 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001263 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 return self.issue
1265
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001266 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001268 issue = self.GetIssue()
1269 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001270 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001271 server = self.GetCodereviewServer()
1272 if short:
1273 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1274 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275
Edward Lemur6c6827c2020-02-06 21:15:18 +00001276 def FetchDescription(self, pretty=False):
1277 assert self.GetIssue(), 'issue is required to query Gerrit'
1278
Edward Lemur9aa1a962020-02-25 00:58:38 +00001279 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001280 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1281 current_rev = data['current_revision']
1282 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001283
1284 if not pretty:
1285 return self.description
1286
1287 # Set width to 72 columns + 2 space indent.
1288 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1289 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1290 lines = self.description.splitlines()
1291 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292
1293 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001294 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001295 if self.patchset is None and not self.lookedup_patchset:
Edward Lemur85153282020-02-14 22:06:29 +00001296 self.patchset = self._GitGetBranchConfigValue(self.PatchsetConfigKey())
1297 if self.patchset is not None:
1298 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001299 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 return self.patchset
1301
Edward Lemur9aa1a962020-02-25 00:58:38 +00001302 def GetAuthor(self):
1303 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001306 """Set this branch's patchset. If patchset=0, clears the patchset."""
1307 assert self.GetBranch()
1308 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001309 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001310 else:
1311 self.patchset = int(patchset)
1312 self._GitSetBranchConfigValue(
Edward Lemur85153282020-02-14 22:06:29 +00001313 self.PatchsetConfigKey(), str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001315 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001316 """Set this branch's issue. If issue isn't given, clears the issue."""
1317 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001319 issue = int(issue)
1320 self._GitSetBranchConfigValue(
Edward Lemur85153282020-02-14 22:06:29 +00001321 self.IssueConfigKey(), str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001322 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001323 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001324 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001325 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001326 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001327 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 else:
tandrii5d48c322016-08-18 16:19:37 -07001329 # Reset all of these just to be clean.
1330 reset_suffixes = [
1331 'last-upload-hash',
Edward Lemur125d60a2019-09-13 18:25:41 +00001332 self.IssueConfigKey(),
1333 self.PatchsetConfigKey(),
1334 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001335 ] + self._PostUnsetIssueProperties()
1336 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001337 try:
1338 self._GitSetBranchConfigValue(prop, None)
1339 except subprocess2.CalledProcessError:
1340 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001341 msg = RunGit(['log', '-1', '--format=%B']).strip()
1342 if msg and git_footers.get_footer_change_id(msg):
1343 print('WARNING: The change patched into this branch has a Change-Id. '
1344 'Removing it.')
1345 RunGit(['commit', '--amend', '-m',
1346 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001347 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001349 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
Edward Lemura12175c2020-03-09 16:58:26 +00001351 def GetChange(self, upstream_branch, description):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001352 if not self.GitSanityChecks(upstream_branch):
1353 DieWithError('\nGit sanity check failure')
1354
Edward Lemur7f6dec02020-02-06 20:23:58 +00001355 root = settings.GetRoot()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001356 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001357 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001358 try:
Edward Lemur7f6dec02020-02-06 20:23:58 +00001359 files = scm.GIT.CaptureStatus(root, upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001360 except subprocess2.CalledProcessError:
1361 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001362 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001363 'This branch probably doesn\'t exist anymore. To reset the\n'
1364 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001365 ' git branch --set-upstream-to origin/master %s\n'
1366 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001367 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001368
maruel@chromium.org52424302012-08-29 15:14:30 +00001369 issue = self.GetIssue()
1370 patchset = self.GetPatchset()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001371
Edward Lemur9aa1a962020-02-25 00:58:38 +00001372 author = self.GetAuthor()
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001373 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001374 name,
1375 description,
Edward Lemur7f6dec02020-02-06 20:23:58 +00001376 root,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001377 files,
1378 issue,
1379 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001380 author,
1381 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001382
dsansomee2d6fd92016-09-08 00:10:47 -07001383 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001384 assert self.GetIssue(), 'issue is required to update description'
1385
1386 if gerrit_util.HasPendingChangeEdit(
1387 self._GetGerritHost(), self._GerritChangeIdentifier()):
1388 if not force:
1389 confirm_or_exit(
1390 'The description cannot be modified while the issue has a pending '
1391 'unpublished edit. Either publish the edit in the Gerrit web UI '
1392 'or delete it.\n\n', action='delete the unpublished edit')
1393
1394 gerrit_util.DeletePendingChangeEdit(
1395 self._GetGerritHost(), self._GerritChangeIdentifier())
1396 gerrit_util.SetCommitMessage(
1397 self._GetGerritHost(), self._GerritChangeIdentifier(),
1398 description, notify='NONE')
1399
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001400 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001401
Edward Lemur75526302020-02-27 22:31:05 +00001402 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001403 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001404 '--root', settings.GetRoot(),
1405 '--upstream', upstream,
1406 ]
1407
1408 args.extend(['--verbose'] * verbose)
1409
Edward Lemur99df04e2020-03-05 19:39:43 +00001410 author = self.GetAuthor()
1411 gerrit_url = self.GetCodereviewServer()
Edward Lemur227d5102020-02-25 23:45:35 +00001412 issue = self.GetIssue()
1413 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001414 if author:
1415 args.extend(['--author', author])
1416 if gerrit_url:
1417 args.extend(['--gerrit_url', gerrit_url])
Edward Lemur227d5102020-02-25 23:45:35 +00001418 if issue:
1419 args.extend(['--issue', str(issue)])
1420 if patchset:
1421 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001422
Edward Lemur75526302020-02-27 22:31:05 +00001423 return args
1424
1425 def RunHook(
1426 self, committing, may_prompt, verbose, parallel, upstream, description,
1427 all_files):
1428 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1429 args = self._GetCommonPresubmitArgs(verbose, upstream)
1430 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001431 if may_prompt:
1432 args.append('--may_prompt')
1433 if parallel:
1434 args.append('--parallel')
1435 if all_files:
1436 args.append('--all_files')
1437
1438 with gclient_utils.temporary_file() as description_file:
1439 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001440
1441 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001442 args.extend(['--json_output', json_output])
1443 args.extend(['--description_file', description_file])
1444
1445 start = time_time()
1446 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1447 exit_code = p.wait()
1448 metrics.collector.add_repeated('sub_commands', {
1449 'command': 'presubmit',
1450 'execution_time': time_time() - start,
1451 'exit_code': exit_code,
1452 })
1453
1454 if exit_code:
1455 sys.exit(exit_code)
1456
1457 json_results = gclient_utils.FileRead(json_output)
1458 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001459
Edward Lemur75526302020-02-27 22:31:05 +00001460 def RunPostUploadHook(self, verbose, upstream, description):
1461 args = self._GetCommonPresubmitArgs(verbose, upstream)
1462 args.append('--post_upload')
1463
1464 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001465 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001466 args.extend(['--description_file', description_file])
1467 p = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1468 p.wait()
1469
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001470 def CMDUpload(self, options, git_diff_args, orig_args):
1471 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001472 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001473 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001474 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001475 else:
1476 if self.GetBranch() is None:
1477 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1478
1479 # Default to diffing against common ancestor of upstream branch
1480 base_branch = self.GetCommonAncestorWithUpstream()
1481 git_diff_args = [base_branch, 'HEAD']
1482
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001483 # Fast best-effort checks to abort before running potentially expensive
1484 # hooks if uploading is likely to fail anyway. Passing these checks does
1485 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001486 self.EnsureAuthenticated(force=options.force)
1487 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001488
Edward Lemura12175c2020-03-09 16:58:26 +00001489 # Get description message for upload.
1490 if self.GetIssue():
1491 description = self.FetchDescription()
1492 elif options.message:
1493 description = options.message
1494 else:
1495 description = _create_description_from_log(git_diff_args)
1496 if options.title and options.squash:
1497 description = options.title + '\n\n' + message
1498
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001499 # Apply watchlists on upload.
Edward Lemura12175c2020-03-09 16:58:26 +00001500 change = self.GetChange(base_branch, description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001501 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1502 files = [f.LocalPath() for f in change.AffectedFiles()]
1503 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001504 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001505
Edward Lemur75526302020-02-27 22:31:05 +00001506 if options.reviewers or options.tbrs or options.add_owners_to:
1507 # Set the reviewer list now so that presubmit checks can access it.
1508 change_description = ChangeDescription(description)
1509 change_description.update_reviewers(options.reviewers,
1510 options.tbrs,
1511 options.add_owners_to,
1512 change)
1513 description = change_description.description
1514
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001515 if not options.bypass_hooks:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001516 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001517 may_prompt=not options.force,
1518 verbose=options.verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00001519 parallel=options.parallel,
1520 upstream=base_branch,
1521 description=description,
1522 all_files=False)
1523 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001524
Aaron Gable13101a62018-02-09 13:20:41 -08001525 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001526 ret = self.CMDUploadChange(
1527 options, git_diff_args, custom_cl_base, change, description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001528 if not ret:
Edward Lemur85153282020-02-14 22:06:29 +00001529 self._GitSetBranchConfigValue(
1530 'last-upload-hash', RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001531 # Run post upload hooks, if specified.
1532 if settings.GetRunPostUploadHook():
Edward Lemur75526302020-02-27 22:31:05 +00001533 self.RunPostUploadHook(options.verbose, base_branch, description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001534
1535 # Upload all dependencies if specified.
1536 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001537 print()
1538 print('--dependencies has been specified.')
1539 print('All dependent local branches will be re-uploaded.')
1540 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001541 # Remove the dependencies flag from args so that we do not end up in a
1542 # loop.
1543 orig_args.remove('--dependencies')
1544 ret = upload_branch_deps(self, orig_args)
1545 return ret
1546
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001547 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001548 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001549
1550 Issue must have been already uploaded and known.
1551 """
1552 assert new_state in _CQState.ALL_STATES
1553 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001554 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001555 vote_map = {
1556 _CQState.NONE: 0,
1557 _CQState.DRY_RUN: 1,
1558 _CQState.COMMIT: 2,
1559 }
1560 labels = {'Commit-Queue': vote_map[new_state]}
1561 notify = False if new_state == _CQState.DRY_RUN else None
1562 gerrit_util.SetReview(
1563 self._GetGerritHost(), self._GerritChangeIdentifier(),
1564 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001565 return 0
1566 except KeyboardInterrupt:
1567 raise
1568 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001569 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001570 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001571 ' * Your project has no CQ,\n'
1572 ' * You don\'t have permission to change the CQ state,\n'
1573 ' * There\'s a bug in this code (see stack trace below).\n'
1574 'Consider specifying which bots to trigger manually or asking your '
1575 'project owners for permissions or contacting Chrome Infra at:\n'
1576 'https://www.chromium.org/infra\n\n' %
1577 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001578 # Still raise exception so that stack trace is printed.
1579 raise
1580
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001581 def _GetGerritHost(self):
1582 # Lazy load of configs.
1583 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001584 if self._gerrit_host and '.' not in self._gerrit_host:
1585 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1586 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001587 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001588 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001589 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001590 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001591 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1592 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001593 return self._gerrit_host
1594
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001595 def _GetGitHost(self):
1596 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001597 remote_url = self.GetRemoteUrl()
1598 if not remote_url:
1599 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001600 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001601
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001602 def GetCodereviewServer(self):
1603 if not self._gerrit_server:
1604 # If we're on a branch then get the server potentially associated
1605 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001606 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001607 self._gerrit_server = self._GitGetBranchConfigValue(
1608 self.CodereviewServerConfigKey())
1609 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001610 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001611 if not self._gerrit_server:
1612 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1613 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001614 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 parts[0] = parts[0] + '-review'
1616 self._gerrit_host = '.'.join(parts)
1617 self._gerrit_server = 'https://%s' % self._gerrit_host
1618 return self._gerrit_server
1619
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001620 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001621 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001622 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001623 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001624 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001625 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001626 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001627 if project.endswith('.git'):
1628 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001629 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1630 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1631 # gitiles/git-over-https protocol. E.g.,
1632 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1633 # as
1634 # https://chromium.googlesource.com/v8/v8
1635 if project.startswith('a/'):
1636 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001637 return project
1638
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001639 def _GerritChangeIdentifier(self):
1640 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1641
1642 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001643 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001644 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001645 project = self._GetGerritProject()
1646 if project:
1647 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1648 # Fall back on still unique, but less efficient change number.
1649 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001650
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001651 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001652 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001653 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654
tandrii5d48c322016-08-18 16:19:37 -07001655 @classmethod
1656 def PatchsetConfigKey(cls):
1657 return 'gerritpatchset'
1658
1659 @classmethod
1660 def CodereviewServerConfigKey(cls):
1661 return 'gerritserver'
1662
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001663 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001664 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001665 if settings.GetGerritSkipEnsureAuthenticated():
1666 # For projects with unusual authentication schemes.
1667 # See http://crbug.com/603378.
1668 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001669
1670 # Check presence of cookies only if using cookies-based auth method.
1671 cookie_auth = gerrit_util.Authenticator.get()
1672 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001673 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001674
Florian Mayerae510e82020-01-30 21:04:48 +00001675 remote_url = self.GetRemoteUrl()
1676 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001677 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00001678 return
1679 if urllib.parse.urlparse(remote_url).scheme != 'https':
Josip906bfde2020-01-31 22:38:49 +00001680 logging.warning('Ignoring branch %(branch)s with non-https remote '
1681 '%(remote)s', {
1682 'branch': self.branch,
1683 'remote': self.GetRemoteUrl()
1684 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00001685 return
1686
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001687 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001688 self.GetCodereviewServer()
1689 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001690 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001691
1692 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1693 git_auth = cookie_auth.get_auth_header(git_host)
1694 if gerrit_auth and git_auth:
1695 if gerrit_auth == git_auth:
1696 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001697 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001698 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001699 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001700 ' %s\n'
1701 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001702 ' Consider running the following command:\n'
1703 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001704 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001705 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001706 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001707 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001708 cookie_auth.get_new_password_message(git_host)))
1709 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001710 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001711 return
1712 else:
1713 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001714 ([] if gerrit_auth else [self._gerrit_host]) +
1715 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001716 DieWithError('Credentials for the following hosts are required:\n'
1717 ' %s\n'
1718 'These are read from %s (or legacy %s)\n'
1719 '%s' % (
1720 '\n '.join(missing),
1721 cookie_auth.get_gitcookies_path(),
1722 cookie_auth.get_netrc_path(),
1723 cookie_auth.get_new_password_message(git_host)))
1724
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001725 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001726 if not self.GetIssue():
1727 return
1728
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001729 status = self._GetChangeDetail()['status']
1730 if status in ('MERGED', 'ABANDONED'):
1731 DieWithError('Change %s has been %s, new uploads are not allowed' %
1732 (self.GetIssueURL(),
1733 'submitted' if status == 'MERGED' else 'abandoned'))
1734
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001735 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1736 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1737 # Apparently this check is not very important? Otherwise get_auth_email
1738 # could have been added to other implementations of Authenticator.
1739 cookies_auth = gerrit_util.Authenticator.get()
1740 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001741 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001742
1743 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001744 if self.GetIssueOwner() == cookies_user:
1745 return
1746 logging.debug('change %s owner is %s, cookies user is %s',
1747 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001748 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001749 # so ask what Gerrit thinks of this user.
1750 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1751 if details['email'] == self.GetIssueOwner():
1752 return
1753 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001754 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001755 'as %s.\n'
1756 'Uploading may fail due to lack of permissions.' %
1757 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1758 confirm_or_exit(action='upload')
1759
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001760 def _PostUnsetIssueProperties(self):
1761 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001762 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001763
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001764 def GetGerritObjForPresubmit(self):
1765 return presubmit_support.GerritAccessor(self._GetGerritHost())
1766
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001768 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001769 or CQ status, assuming adherence to a common workflow.
1770
1771 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001772 * 'error' - error from review tool (including deleted issues)
1773 * 'unsent' - no reviewers added
1774 * 'waiting' - waiting for review
1775 * 'reply' - waiting for uploader to reply to review
1776 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001777 * 'dry-run' - dry-running in the CQ
1778 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001779 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001780 """
1781 if not self.GetIssue():
1782 return None
1783
1784 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001785 data = self._GetChangeDetail([
1786 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001787 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001788 return 'error'
1789
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001790 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001791 return 'closed'
1792
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001793 cq_label = data['labels'].get('Commit-Queue', {})
1794 max_cq_vote = 0
1795 for vote in cq_label.get('all', []):
1796 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1797 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001798 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001799 if max_cq_vote == 1:
1800 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001801
Aaron Gable9ab38c62017-04-06 14:36:33 -07001802 if data['labels'].get('Code-Review', {}).get('approved'):
1803 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001804
1805 if not data.get('reviewers', {}).get('REVIEWER', []):
1806 return 'unsent'
1807
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001808 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001809 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001810 while messages:
1811 m = messages.pop()
1812 if m.get('tag', '').startswith('autogenerated:cq:'):
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001813 # Ignore replies from CQ.
1814 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00001815 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001816 # Most recent message was by owner.
1817 return 'waiting'
1818 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001819 # Some reply from non-owner.
1820 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001821
1822 # Somehow there are no messages even though there are reviewers.
1823 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824
1825 def GetMostRecentPatchset(self):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001826 if not self.GetIssue():
1827 return None
1828
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001829 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001830 patchset = data['revisions'][data['current_revision']]['_number']
1831 self.SetPatchset(patchset)
1832 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833
Aaron Gable636b13f2017-07-14 10:42:48 -07001834 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001835 gerrit_util.SetReview(
1836 self._GetGerritHost(), self._GerritChangeIdentifier(),
1837 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001838
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001839 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001840 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001841 # CURRENT_REVISION is included to get the latest patchset so that
1842 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001843 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001844 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1845 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001846 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001847 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001848 robot_file_comments = gerrit_util.GetChangeRobotComments(
1849 self._GetGerritHost(), self._GerritChangeIdentifier())
1850
1851 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001852 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001853 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001854 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001855 line_comments = file_comments.setdefault(path, [])
1856 line_comments.extend(
1857 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001858
1859 # Build dictionary of file comments for easy access and sorting later.
1860 # {author+date: {path: {patchset: {line: url+message}}}}
1861 comments = collections.defaultdict(
1862 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001863 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001864 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001865 tag = comment.get('tag', '')
1866 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001867 continue
1868 key = (comment['author']['email'], comment['updated'])
1869 if comment.get('side', 'REVISION') == 'PARENT':
1870 patchset = 'Base'
1871 else:
1872 patchset = 'PS%d' % comment['patch_set']
1873 line = comment.get('line', 0)
1874 url = ('https://%s/c/%s/%s/%s#%s%s' %
1875 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1876 'b' if comment.get('side') == 'PARENT' else '',
1877 str(line) if line else ''))
1878 comments[key][path][patchset][line] = (url, comment['message'])
1879
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001880 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001881 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001882 summary = self._BuildCommentSummary(msg, comments, readable)
1883 if summary:
1884 summaries.append(summary)
1885 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001886
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001887 @staticmethod
1888 def _BuildCommentSummary(msg, comments, readable):
1889 key = (msg['author']['email'], msg['date'])
1890 # Don't bother showing autogenerated messages that don't have associated
1891 # file or line comments. this will filter out most autogenerated
1892 # messages, but will keep robot comments like those from Tricium.
1893 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1894 if is_autogenerated and not comments.get(key):
1895 return None
1896 message = msg['message']
1897 # Gerrit spits out nanoseconds.
1898 assert len(msg['date'].split('.')[-1]) == 9
1899 date = datetime.datetime.strptime(msg['date'][:-3],
1900 '%Y-%m-%d %H:%M:%S.%f')
1901 if key in comments:
1902 message += '\n'
1903 for path, patchsets in sorted(comments.get(key, {}).items()):
1904 if readable:
1905 message += '\n%s' % path
1906 for patchset, lines in sorted(patchsets.items()):
1907 for line, (url, content) in sorted(lines.items()):
1908 if line:
1909 line_str = 'Line %d' % line
1910 path_str = '%s:%d:' % (path, line)
1911 else:
1912 line_str = 'File comment'
1913 path_str = '%s:0:' % path
1914 if readable:
1915 message += '\n %s, %s: %s' % (patchset, line_str, url)
1916 message += '\n %s\n' % content
1917 else:
1918 message += '\n%s ' % path_str
1919 message += '\n%s\n' % content
1920
1921 return _CommentSummary(
1922 date=date,
1923 message=message,
1924 sender=msg['author']['email'],
1925 autogenerated=is_autogenerated,
1926 # These could be inferred from the text messages and correlated with
1927 # Code-Review label maximum, however this is not reliable.
1928 # Leaving as is until the need arises.
1929 approval=False,
1930 disapproval=False,
1931 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001933 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001934 gerrit_util.AbandonChange(
1935 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001936
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001937 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001938 gerrit_util.SubmitChange(
1939 self._GetGerritHost(), self._GerritChangeIdentifier(),
1940 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001942 def _GetChangeDetail(self, options=None):
1943 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001944 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001945 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001946
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001947 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001948 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001949 options.append('CURRENT_COMMIT')
1950
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001951 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001952 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001953 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001954
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001955 for cached_options_set, data in self._detail_cache.get(cache_key, []):
1956 # Assumption: data fetched before with extra options is suitable
1957 # for return for a smaller set of options.
1958 # For example, if we cached data for
1959 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
1960 # and request is for options=[CURRENT_REVISION],
1961 # THEN we can return prior cached data.
1962 if options_set.issubset(cached_options_set):
1963 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001964
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001965 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001966 data = gerrit_util.GetChangeDetail(
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001967 self._GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001968 except gerrit_util.GerritError as e:
1969 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001970 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01001971 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001972
Edward Lesmes7677e5c2020-02-19 20:39:03 +00001973 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07001974 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001975
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00001976 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001977 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001978 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001979 data = gerrit_util.GetChangeCommit(
1980 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001981 except gerrit_util.GerritError as e:
1982 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00001983 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07001984 raise
agable32978d92016-11-01 12:55:02 -07001985 return data
1986
Karen Qian40c19422019-03-13 21:28:29 +00001987 def _IsCqConfigured(self):
1988 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00001989 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00001990
Olivier Robin75ee7252018-04-13 10:02:56 +02001991 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001992 if git_common.is_dirty_git_tree('land'):
1993 return 1
Karen Qian40c19422019-03-13 21:28:29 +00001994
tandriid60367b2016-06-22 05:25:12 -07001995 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00001996 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001997 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001998 'which can test and land changes for you. '
1999 'Are you sure you wish to bypass it?\n',
2000 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002001 differs = True
tandriic4344b52016-08-29 06:04:54 -07002002 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002003 # Note: git diff outputs nothing if there is no diff.
2004 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002005 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002006 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002007 if detail['current_revision'] == last_upload:
2008 differs = False
2009 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002010 print('WARNING: Local branch contents differ from latest uploaded '
2011 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002012 if differs:
2013 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002014 confirm_or_exit(
2015 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2016 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002017 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002018 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00002019 upstream = self.GetCommonAncestorWithUpstream()
2020 if self.GetIssue():
2021 description = self.FetchDescription()
2022 else:
Edward Lemura12175c2020-03-09 16:58:26 +00002023 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00002024 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002025 committing=True,
2026 may_prompt=not force,
2027 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00002028 parallel=parallel,
2029 upstream=upstream,
2030 description=description,
2031 all_files=False)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002032
2033 self.SubmitIssue(wait_for_merge=True)
2034 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002035 links = self._GetChangeCommit().get('web_links', [])
2036 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002037 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002038 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002039 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002040 return 0
2041
Edward Lemurf38bc172019-09-03 21:02:13 +00002042 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002043 assert parsed_issue_arg.valid
2044
Edward Lemur125d60a2019-09-13 18:25:41 +00002045 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002046
2047 if parsed_issue_arg.hostname:
2048 self._gerrit_host = parsed_issue_arg.hostname
2049 self._gerrit_server = 'https://%s' % self._gerrit_host
2050
tandriic2405f52016-10-10 08:13:15 -07002051 try:
2052 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002053 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002054 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002055
2056 if not parsed_issue_arg.patchset:
2057 # Use current revision by default.
2058 revision_info = detail['revisions'][detail['current_revision']]
2059 patchset = int(revision_info['_number'])
2060 else:
2061 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002062 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002063 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2064 break
2065 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002066 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002067 (parsed_issue_arg.patchset, self.GetIssue()))
2068
Edward Lemur125d60a2019-09-13 18:25:41 +00002069 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002070 if remote_url.endswith('.git'):
2071 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002072 remote_url = remote_url.rstrip('/')
2073
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002074 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002075 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002076
2077 if remote_url != fetch_info['url']:
2078 DieWithError('Trying to patch a change from %s but this repo appears '
2079 'to be %s.' % (fetch_info['url'], remote_url))
2080
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002082
Aaron Gable62619a32017-06-16 08:22:09 -07002083 if force:
2084 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2085 print('Checked out commit for change %i patchset %i locally' %
2086 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002087 elif nocommit:
2088 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2089 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002090 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002091 RunGit(['cherry-pick', 'FETCH_HEAD'])
2092 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002093 (parsed_issue_arg.issue, patchset))
2094 print('Note: this created a local commit which does not have '
2095 'the same hash as the one uploaded for review. This will make '
2096 'uploading changes based on top of this branch difficult.\n'
2097 'If you want to do that, use "git cl patch --force" instead.')
2098
Stefan Zagerd08043c2017-10-12 12:07:02 -07002099 if self.GetBranch():
2100 self.SetIssue(parsed_issue_arg.issue)
2101 self.SetPatchset(patchset)
2102 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2103 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2104 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2105 else:
2106 print('WARNING: You are in detached HEAD state.\n'
2107 'The patch has been applied to your checkout, but you will not be '
2108 'able to upload a new patch set to the gerrit issue.\n'
2109 'Try using the \'-b\' option if you would like to work on a '
2110 'branch and/or upload a new patch set.')
2111
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 return 0
2113
tandrii16e0b4e2016-06-07 10:34:28 -07002114 def _GerritCommitMsgHookCheck(self, offer_removal):
2115 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2116 if not os.path.exists(hook):
2117 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002118 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2119 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002120 data = gclient_utils.FileRead(hook)
2121 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2122 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002123 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002124 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002125 'and may interfere with it in subtle ways.\n'
2126 'We recommend you remove the commit-msg hook.')
2127 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002128 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002129 gclient_utils.rm_file_or_tree(hook)
2130 print('Gerrit commit-msg hook removed.')
2131 else:
2132 print('OK, will keep Gerrit commit-msg hook in place.')
2133
Edward Lemur1b52d872019-05-09 21:12:12 +00002134 def _CleanUpOldTraces(self):
2135 """Keep only the last |MAX_TRACES| traces."""
2136 try:
2137 traces = sorted([
2138 os.path.join(TRACES_DIR, f)
2139 for f in os.listdir(TRACES_DIR)
2140 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2141 and not f.startswith('tmp'))
2142 ])
2143 traces_to_delete = traces[:-MAX_TRACES]
2144 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002145 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002146 except OSError:
2147 print('WARNING: Failed to remove old git traces from\n'
2148 ' %s'
2149 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002150
Edward Lemur5737f022019-05-17 01:24:00 +00002151 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002152 """Zip and write the git push traces stored in traces_dir."""
2153 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002154 traces_zip = trace_name + '-traces'
2155 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002156 # Create a temporary dir to store git config and gitcookies in. It will be
2157 # compressed and stored next to the traces.
2158 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002159 git_info_zip = trace_name + '-git-info'
2160
Edward Lemur5737f022019-05-17 01:24:00 +00002161 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002162 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002163 git_push_metadata['now'] = git_push_metadata['now'].decode(
2164 sys.stdin.encoding)
2165
Edward Lemur1b52d872019-05-09 21:12:12 +00002166 git_push_metadata['trace_name'] = trace_name
2167 gclient_utils.FileWrite(
2168 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2169
2170 # Keep only the first 6 characters of the git hashes on the packet
2171 # trace. This greatly decreases size after compression.
2172 packet_traces = os.path.join(traces_dir, 'trace-packet')
2173 if os.path.isfile(packet_traces):
2174 contents = gclient_utils.FileRead(packet_traces)
2175 gclient_utils.FileWrite(
2176 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2177 shutil.make_archive(traces_zip, 'zip', traces_dir)
2178
2179 # Collect and compress the git config and gitcookies.
2180 git_config = RunGit(['config', '-l'])
2181 gclient_utils.FileWrite(
2182 os.path.join(git_info_dir, 'git-config'),
2183 git_config)
2184
2185 cookie_auth = gerrit_util.Authenticator.get()
2186 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2187 gitcookies_path = cookie_auth.get_gitcookies_path()
2188 if os.path.isfile(gitcookies_path):
2189 gitcookies = gclient_utils.FileRead(gitcookies_path)
2190 gclient_utils.FileWrite(
2191 os.path.join(git_info_dir, 'gitcookies'),
2192 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2193 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2194
Edward Lemur1b52d872019-05-09 21:12:12 +00002195 gclient_utils.rmtree(git_info_dir)
2196
2197 def _RunGitPushWithTraces(
2198 self, change_desc, refspec, refspec_opts, git_push_metadata):
2199 """Run git push and collect the traces resulting from the execution."""
2200 # Create a temporary directory to store traces in. Traces will be compressed
2201 # and stored in a 'traces' dir inside depot_tools.
2202 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002203 trace_name = os.path.join(
2204 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002205
2206 env = os.environ.copy()
2207 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2208 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002209 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002210 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2211 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2212 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2213
2214 try:
2215 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002216 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002217 before_push = time_time()
2218 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002219 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002220 env=env,
2221 print_stdout=True,
2222 # Flush after every line: useful for seeing progress when running as
2223 # recipe.
2224 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002225 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002226 except subprocess2.CalledProcessError as e:
2227 push_returncode = e.returncode
2228 DieWithError('Failed to create a change. Please examine output above '
2229 'for the reason of the failure.\n'
2230 'Hint: run command below to diagnose common Git/Gerrit '
2231 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002232 ' git cl creds-check\n'
2233 '\n'
2234 'If git-cl is not working correctly, file a bug under the '
2235 'Infra>SDK component including the files below.\n'
2236 'Review the files before upload, since they might contain '
2237 'sensitive information.\n'
2238 'Set the Restrict-View-Google label so that they are not '
2239 'publicly accessible.\n'
2240 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002241 change_desc)
2242 finally:
2243 execution_time = time_time() - before_push
2244 metrics.collector.add_repeated('sub_commands', {
2245 'command': 'git push',
2246 'execution_time': execution_time,
2247 'exit_code': push_returncode,
2248 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2249 })
2250
Edward Lemur1b52d872019-05-09 21:12:12 +00002251 git_push_metadata['execution_time'] = execution_time
2252 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002253 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002254
Edward Lemur1b52d872019-05-09 21:12:12 +00002255 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002256 gclient_utils.rmtree(traces_dir)
2257
2258 return push_stdout
2259
Edward Lemura12175c2020-03-09 16:58:26 +00002260 def CMDUploadChange(
2261 self, options, git_diff_args, custom_cl_base, change, message):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002262 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002263 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002264 # Load default for user, repo, squash=true, in this order.
2265 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002266
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002267 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002268 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002269 # This may be None; default fallback value is determined in logic below.
2270 title = options.title
2271
Dominic Battre7d1c4842017-10-27 09:17:28 +02002272 # Extract bug number from branch name.
2273 bug = options.bug
Dan Beamd8b04ca2019-10-10 21:23:26 +00002274 fixed = options.fixed
2275 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
2276 self.GetBranch())
2277 if not bug and not fixed and match:
2278 if match.group('type') == 'bug':
2279 bug = match.group('bugnum')
2280 else:
2281 fixed = match.group('bugnum')
Dominic Battre7d1c4842017-10-27 09:17:28 +02002282
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002284 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 if self.GetIssue():
Aaron Gableb56ad332017-01-06 15:24:31 -08002286 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002287 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002288 # When uploading a subsequent patchset, -m|--message is taken
2289 # as the patchset title if --title was not provided.
2290 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002291 else:
2292 default_title = RunGit(
2293 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002294 if options.force:
2295 title = default_title
2296 else:
2297 title = ask_for_data(
2298 'Title for patchset [%s]: ' % default_title) or default_title
Josipe827b0f2020-01-30 00:07:20 +00002299
2300 # User requested to change description
2301 if options.edit_description:
2302 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
2303 change_desc.prompt()
2304 message = change_desc.description
2305
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002306 change_id = self._GetChangeDetail()['change_id']
Anthony Polito8b955342019-09-24 19:01:36 +00002307
Edward Lemur5fb22242020-03-12 22:05:13 +00002308 # Make sure that the Change-Id in the description matches the one
2309 # fetched from Gerrit.
2310 footer_change_ids = git_footers.get_footer_change_id(message)
2311 if footer_change_ids != [change_id]:
2312 if footer_change_ids:
2313 # Remove any existing Change-Id footers since they don't match the
2314 # expected change_id footer.
2315 message = git_footers.remove_footer(message, 'Change-Id')
2316 # Add the expected Change-Id footer.
2317 message = git_footers.add_footer_change_id(message, change_id)
2318 print('WARNING: Change-Id has been set to Change-Id fetched from '
2319 'Gerrit. Use `git cl issue 0` if you want to clear it and '
2320 'set a new one.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00002321 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
Aaron Gableb56ad332017-01-06 15:24:31 -08002322 else: # if not self.GetIssue()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002323 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002324 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002325 change_desc.prompt()
2326
Aaron Gableb56ad332017-01-06 15:24:31 -08002327 # On first upload, patchset title is always this string, while
2328 # --title flag gets converted to first line of message.
2329 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002330 if not change_desc.description:
2331 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002332 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002333 if len(change_ids) > 1:
2334 DieWithError('too many Change-Id footers, at most 1 allowed.')
2335 if not change_ids:
2336 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002337 change_desc.set_description(git_footers.add_footer_change_id(
2338 change_desc.description,
2339 GenerateGerritChangeId(change_desc.description)))
2340 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002341 assert len(change_ids) == 1
2342 change_id = change_ids[0]
2343
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002344 if options.reviewers or options.tbrs or options.add_owners_to:
2345 change_desc.update_reviewers(options.reviewers, options.tbrs,
2346 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002347 if options.preserve_tryjobs:
2348 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002349
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002350 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002351 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2352 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002353 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002354 with gclient_utils.temporary_file() as desc_tempfile:
2355 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2356 ref_to_push = RunGit(
2357 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002358 else: # if not options.squash
Edward Lemura12175c2020-03-09 16:58:26 +00002359 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002360 if not change_desc.description:
2361 DieWithError("Description is empty. Aborting...")
2362
2363 if not git_footers.get_footer_change_id(change_desc.description):
2364 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002365 change_desc.set_description(
Edward Lemura12175c2020-03-09 16:58:26 +00002366 self._AddChangeIdToCommitMessage(message, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002367 if options.reviewers or options.tbrs or options.add_owners_to:
2368 change_desc.update_reviewers(options.reviewers, options.tbrs,
2369 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002370 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002371 # For no-squash mode, we assume the remote called "origin" is the one we
2372 # want. It is not worthwhile to support different workflows for
2373 # no-squash mode.
2374 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002375 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2376
2377 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002378 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002379 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2380 ref_to_push)]).splitlines()
2381 if len(commits) > 1:
2382 print('WARNING: This will upload %d commits. Run the following command '
2383 'to see which commits will be uploaded: ' % len(commits))
2384 print('git log %s..%s' % (parent, ref_to_push))
2385 print('You can also use `git squash-branch` to squash these into a '
2386 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002387 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002388
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002389 if options.reviewers or options.tbrs or options.add_owners_to:
2390 change_desc.update_reviewers(options.reviewers, options.tbrs,
2391 options.add_owners_to, change)
2392
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002393 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002394 cc = []
2395 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2396 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2397 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002398 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002399 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002400 if options.cc:
2401 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002402 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002403 if change_desc.get_cced():
2404 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002405 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2406 valid_accounts = set(reviewers + cc)
2407 # TODO(crbug/877717): relax this for all hosts.
2408 else:
2409 valid_accounts = gerrit_util.ValidAccounts(
2410 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002411 logging.info('accounts %s are recognized, %s invalid',
2412 sorted(valid_accounts),
2413 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002414
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002415 # Extra options that can be specified at push time. Doc:
2416 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002417 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002418
Aaron Gable844cf292017-06-28 11:32:59 -07002419 # By default, new changes are started in WIP mode, and subsequent patchsets
2420 # don't send email. At any time, passing --send-mail will mark the change
2421 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002422 if options.send_mail:
2423 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002424 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002425 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002426 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002427 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002428 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002429
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002430 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002431 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002432
Aaron Gable9b713dd2016-12-14 16:04:21 -08002433 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002434 # Punctuation and whitespace in |title| must be percent-encoded.
2435 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002436
agablec6787972016-09-09 16:13:34 -07002437 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002438 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002439
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002440 for r in sorted(reviewers):
2441 if r in valid_accounts:
2442 refspec_opts.append('r=%s' % r)
2443 reviewers.remove(r)
2444 else:
2445 # TODO(tandrii): this should probably be a hard failure.
2446 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2447 % r)
2448 for c in sorted(cc):
2449 # refspec option will be rejected if cc doesn't correspond to an
2450 # account, even though REST call to add such arbitrary cc may succeed.
2451 if c in valid_accounts:
2452 refspec_opts.append('cc=%s' % c)
2453 cc.remove(c)
2454
rmistry9eadede2016-09-19 11:22:43 -07002455 if options.topic:
2456 # Documentation on Gerrit topics is here:
2457 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002458 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002459
Edward Lemur687ca902018-12-05 02:30:30 +00002460 if options.enable_auto_submit:
2461 refspec_opts.append('l=Auto-Submit+1')
2462 if options.use_commit_queue:
2463 refspec_opts.append('l=Commit-Queue+2')
2464 elif options.cq_dry_run:
2465 refspec_opts.append('l=Commit-Queue+1')
2466
2467 if change_desc.get_reviewers(tbr_only=True):
2468 score = gerrit_util.GetCodeReviewTbrScore(
2469 self._GetGerritHost(),
2470 self._GetGerritProject())
2471 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002472
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002473 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002474 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002475 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002476 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002477 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2478
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002479 refspec_suffix = ''
2480 if refspec_opts:
2481 refspec_suffix = '%' + ','.join(refspec_opts)
2482 assert ' ' not in refspec_suffix, (
2483 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2484 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2485
Edward Lemur1b52d872019-05-09 21:12:12 +00002486 git_push_metadata = {
2487 'gerrit_host': self._GetGerritHost(),
2488 'title': title or '<untitled>',
2489 'change_id': change_id,
2490 'description': change_desc.description,
2491 }
2492 push_stdout = self._RunGitPushWithTraces(
2493 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002494
2495 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002496 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002497 change_numbers = [m.group(1)
2498 for m in map(regex.match, push_stdout.splitlines())
2499 if m]
2500 if len(change_numbers) != 1:
2501 DieWithError(
2502 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002503 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002504 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002505 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002506
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002507 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002508 # GetIssue() is not set in case of non-squash uploads according to tests.
2509 # TODO(agable): non-squash uploads in git cl should be removed.
2510 gerrit_util.AddReviewers(
2511 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002512 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002513 reviewers, cc,
2514 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002515
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002516 return 0
2517
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002518 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2519 change_desc):
2520 """Computes parent of the generated commit to be uploaded to Gerrit.
2521
2522 Returns revision or a ref name.
2523 """
2524 if custom_cl_base:
2525 # Try to avoid creating additional unintended CLs when uploading, unless
2526 # user wants to take this risk.
2527 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2528 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2529 local_ref_of_target_remote])
2530 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002531 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002532 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2533 'If you proceed with upload, more than 1 CL may be created by '
2534 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2535 'If you are certain that specified base `%s` has already been '
2536 'uploaded to Gerrit as another CL, you may proceed.\n' %
2537 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2538 if not force:
2539 confirm_or_exit(
2540 'Do you take responsibility for cleaning up potential mess '
2541 'resulting from proceeding with upload?',
2542 action='upload')
2543 return custom_cl_base
2544
Aaron Gablef97e33d2017-03-30 15:44:27 -07002545 if remote != '.':
2546 return self.GetCommonAncestorWithUpstream()
2547
2548 # If our upstream branch is local, we base our squashed commit on its
2549 # squashed version.
2550 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2551
Aaron Gablef97e33d2017-03-30 15:44:27 -07002552 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002553 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002554
2555 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002556 # TODO(tandrii): consider checking parent change in Gerrit and using its
2557 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2558 # the tree hash of the parent branch. The upside is less likely bogus
2559 # requests to reupload parent change just because it's uploadhash is
2560 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002561 parent = RunGit(['config',
2562 'branch.%s.gerritsquashhash' % upstream_branch_name],
2563 error_ok=True).strip()
2564 # Verify that the upstream branch has been uploaded too, otherwise
2565 # Gerrit will create additional CLs when uploading.
2566 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2567 RunGitSilent(['rev-parse', parent + ':'])):
2568 DieWithError(
2569 '\nUpload upstream branch %s first.\n'
2570 'It is likely that this branch has been rebased since its last '
2571 'upload, so you just need to upload it again.\n'
2572 '(If you uploaded it with --no-squash, then branch dependencies '
2573 'are not supported, and you should reupload with --squash.)'
2574 % upstream_branch_name,
2575 change_desc)
2576 return parent
2577
Edward Lemura12175c2020-03-09 16:58:26 +00002578 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002579 """Re-commits using the current message, assumes the commit hook is in
2580 place.
2581 """
Edward Lemura12175c2020-03-09 16:58:26 +00002582 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002583 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002584 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002585 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002586 return new_log_desc
2587 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002588 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002589
tandriie113dfd2016-10-11 10:20:12 -07002590 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002591 try:
2592 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002593 except GerritChangeNotExists:
2594 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002595
2596 if data['status'] in ('ABANDONED', 'MERGED'):
2597 return 'CL %s is closed' % self.GetIssue()
2598
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002599 def GetGerritChange(self, patchset=None):
2600 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002601 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002602 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002603 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002604 data = self._GetChangeDetail(['ALL_REVISIONS'])
2605
2606 assert host and issue and patchset, 'CL must be uploaded first'
2607
2608 has_patchset = any(
2609 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002610 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002611 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002612 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002613 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002614
tandrii8c5a3532016-11-04 07:52:02 -07002615 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002616 'host': host,
2617 'change': issue,
2618 'project': data['project'],
2619 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002620 }
tandriie113dfd2016-10-11 10:20:12 -07002621
tandriide281ae2016-10-12 06:02:30 -07002622 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002623 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002624
Edward Lemur707d70b2018-02-07 00:50:14 +01002625 def GetReviewers(self):
2626 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002627 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002628
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002629
tandriif9aefb72016-07-01 09:06:51 -07002630def _get_bug_line_values(default_project, bugs):
2631 """Given default_project and comma separated list of bugs, yields bug line
2632 values.
2633
2634 Each bug can be either:
2635 * a number, which is combined with default_project
2636 * string, which is left as is.
2637
2638 This function may produce more than one line, because bugdroid expects one
2639 project per line.
2640
2641 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2642 ['v8:123', 'chromium:789']
2643 """
2644 default_bugs = []
2645 others = []
2646 for bug in bugs.split(','):
2647 bug = bug.strip()
2648 if bug:
2649 try:
2650 default_bugs.append(int(bug))
2651 except ValueError:
2652 others.append(bug)
2653
2654 if default_bugs:
2655 default_bugs = ','.join(map(str, default_bugs))
2656 if default_project:
2657 yield '%s:%s' % (default_project, default_bugs)
2658 else:
2659 yield default_bugs
2660 for other in sorted(others):
2661 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2662 yield other
2663
2664
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002665class ChangeDescription(object):
2666 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002667 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002668 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002669 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002670 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002671 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002672 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2673 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00002674 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002675 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002676
Dan Beamd8b04ca2019-10-10 21:23:26 +00002677 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002678 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002679 if bug:
2680 regexp = re.compile(self.BUG_LINE)
2681 prefix = settings.GetBugPrefix()
2682 if not any((regexp.match(line) for line in self._description_lines)):
2683 values = list(_get_bug_line_values(prefix, bug))
2684 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002685 if fixed:
2686 regexp = re.compile(self.FIXED_LINE)
2687 prefix = settings.GetBugPrefix()
2688 if not any((regexp.match(line) for line in self._description_lines)):
2689 values = list(_get_bug_line_values(prefix, fixed))
2690 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002691
agable@chromium.org42c20792013-09-12 17:34:49 +00002692 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002693 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002694 return '\n'.join(self._description_lines)
2695
2696 def set_description(self, desc):
2697 if isinstance(desc, basestring):
2698 lines = desc.splitlines()
2699 else:
2700 lines = [line.rstrip() for line in desc]
2701 while lines and not lines[0]:
2702 lines.pop(0)
2703 while lines and not lines[-1]:
2704 lines.pop(-1)
2705 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002706
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002707 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2708 """Rewrites the R=/TBR= line(s) as a single line each.
2709
2710 Args:
2711 reviewers (list(str)) - list of additional emails to use for reviewers.
2712 tbrs (list(str)) - list of additional emails to use for TBRs.
2713 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2714 the change that are missing OWNER coverage. If this is not None, you
2715 must also pass a value for `change`.
2716 change (Change) - The Change that should be used for OWNERS lookups.
2717 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002718 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002719 assert isinstance(tbrs, list), tbrs
2720
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002721 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002722 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002723
2724 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002725 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002726
2727 reviewers = set(reviewers)
2728 tbrs = set(tbrs)
2729 LOOKUP = {
2730 'TBR': tbrs,
2731 'R': reviewers,
2732 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002733
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002734 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002735 regexp = re.compile(self.R_LINE)
2736 matches = [regexp.match(line) for line in self._description_lines]
2737 new_desc = [l for i, l in enumerate(self._description_lines)
2738 if not matches[i]]
2739 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002740
agable@chromium.org42c20792013-09-12 17:34:49 +00002741 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002742
2743 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002744 for match in matches:
2745 if not match:
2746 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002747 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2748
2749 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002750 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002751 owners_db = owners.Database(change.RepositoryRoot(),
Edward Lemurb7f759f2020-03-04 21:20:56 +00002752 fopen=open, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002753 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002754 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002755 LOOKUP[add_owners_to].update(
2756 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002757
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002758 # If any folks ended up in both groups, remove them from tbrs.
2759 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002760
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002761 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2762 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002763
2764 # Put the new lines in the description where the old first R= line was.
2765 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2766 if 0 <= line_loc < len(self._description_lines):
2767 if new_tbr_line:
2768 self._description_lines.insert(line_loc, new_tbr_line)
2769 if new_r_line:
2770 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002771 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002772 if new_r_line:
2773 self.append_footer(new_r_line)
2774 if new_tbr_line:
2775 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002776
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002777 def set_preserve_tryjobs(self):
2778 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2779 footers = git_footers.parse_footers(self.description)
2780 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2781 if v.lower() == 'true':
2782 return
2783 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2784
Anthony Polito8b955342019-09-24 19:01:36 +00002785 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002786 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002787 self.set_description([
2788 '# Enter a description of the change.',
2789 '# This will be displayed on the codereview site.',
2790 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002791 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002792 '--------------------',
2793 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002794 bug_regexp = re.compile(self.BUG_LINE)
2795 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002796 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002797 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2798 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002799 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002800
agable@chromium.org42c20792013-09-12 17:34:49 +00002801 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002802 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002803 if not content:
2804 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002805 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002806
Bruce Dawson2377b012018-01-11 16:46:49 -08002807 # Strip off comments and default inserted "Bug:" line.
2808 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002809 (line.startswith('#') or
2810 line.rstrip() == "Bug:" or
2811 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002812 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002813 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002814 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002815
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002816 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002817 """Adds a footer line to the description.
2818
2819 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2820 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2821 that Gerrit footers are always at the end.
2822 """
2823 parsed_footer_line = git_footers.parse_footer(line)
2824 if parsed_footer_line:
2825 # Line is a gerrit footer in the form: Footer-Key: any value.
2826 # Thus, must be appended observing Gerrit footer rules.
2827 self.set_description(
2828 git_footers.add_footer(self.description,
2829 key=parsed_footer_line[0],
2830 value=parsed_footer_line[1]))
2831 return
2832
2833 if not self._description_lines:
2834 self._description_lines.append(line)
2835 return
2836
2837 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2838 if gerrit_footers:
2839 # git_footers.split_footers ensures that there is an empty line before
2840 # actual (gerrit) footers, if any. We have to keep it that way.
2841 assert top_lines and top_lines[-1] == ''
2842 top_lines, separator = top_lines[:-1], top_lines[-1:]
2843 else:
2844 separator = [] # No need for separator if there are no gerrit_footers.
2845
2846 prev_line = top_lines[-1] if top_lines else ''
2847 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2848 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2849 top_lines.append('')
2850 top_lines.append(line)
2851 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002852
tandrii99a72f22016-08-17 14:33:24 -07002853 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002854 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002855 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002856 reviewers = [match.group(2).strip()
2857 for match in matches
2858 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002859 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002860
bradnelsond975b302016-10-23 12:20:23 -07002861 def get_cced(self):
2862 """Retrieves the list of reviewers."""
2863 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2864 cced = [match.group(2).strip() for match in matches if match]
2865 return cleanup_list(cced)
2866
Nodir Turakulov23b82142017-11-16 11:04:25 -08002867 def get_hash_tags(self):
2868 """Extracts and sanitizes a list of Gerrit hashtags."""
2869 subject = (self._description_lines or ('',))[0]
2870 subject = re.sub(
2871 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2872
2873 tags = []
2874 start = 0
2875 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2876 while True:
2877 m = bracket_exp.match(subject, start)
2878 if not m:
2879 break
2880 tags.append(self.sanitize_hash_tag(m.group(1)))
2881 start = m.end()
2882
2883 if not tags:
2884 # Try "Tag: " prefix.
2885 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
2886 if m:
2887 tags.append(self.sanitize_hash_tag(m.group(1)))
2888 return tags
2889
2890 @classmethod
2891 def sanitize_hash_tag(cls, tag):
2892 """Returns a sanitized Gerrit hash tag.
2893
2894 A sanitized hashtag can be used as a git push refspec parameter value.
2895 """
2896 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
2897
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002898 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
2899 """Updates this commit description given the parent.
2900
2901 This is essentially what Gnumbd used to do.
2902 Consult https://goo.gl/WMmpDe for more details.
2903 """
2904 assert parent_msg # No, orphan branch creation isn't supported.
2905 assert parent_hash
2906 assert dest_ref
2907 parent_footer_map = git_footers.parse_footers(parent_msg)
2908 # This will also happily parse svn-position, which GnumbD is no longer
2909 # supporting. While we'd generate correct footers, the verifier plugin
2910 # installed in Gerrit will block such commit (ie git push below will fail).
2911 parent_position = git_footers.get_position(parent_footer_map)
2912
2913 # Cherry-picks may have last line obscuring their prior footers,
2914 # from git_footers perspective. This is also what Gnumbd did.
2915 cp_line = None
2916 if (self._description_lines and
2917 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
2918 cp_line = self._description_lines.pop()
2919
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02002920 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002921
2922 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
2923 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02002924 for i, line in enumerate(footer_lines):
2925 k, v = git_footers.parse_footer(line) or (None, None)
2926 if k and k.startswith('Cr-'):
2927 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002928
2929 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01002930 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002931 if parent_position[0] == dest_ref:
2932 # Same branch as parent.
2933 number = int(parent_position[1]) + 1
2934 else:
2935 number = 1 # New branch, and extra lineage.
2936 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
2937 int(parent_position[1])))
2938
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02002939 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
2940 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002941
2942 self._description_lines = top_lines
2943 if cp_line:
2944 self._description_lines.append(cp_line)
2945 if self._description_lines[-1] != '':
2946 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02002947 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002948
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002949
Aaron Gablea1bab272017-04-11 16:38:18 -07002950def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002951 """Retrieves the reviewers that approved a CL from the issue properties with
2952 messages.
2953
2954 Note that the list may contain reviewers that are not committer, thus are not
2955 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07002956
2957 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002958 """
Aaron Gablea1bab272017-04-11 16:38:18 -07002959 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002960 return sorted(
2961 set(
2962 message['sender']
2963 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07002964 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002965 )
2966 )
2967
2968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969def FindCodereviewSettingsFile(filename='codereview.settings'):
2970 """Finds the given file starting in the cwd and going up.
2971
2972 Only looks up to the top of the repository unless an
2973 'inherit-review-settings-ok' file exists in the root of the repository.
2974 """
2975 inherit_ok_file = 'inherit-review-settings-ok'
2976 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002977 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002978 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2979 root = '/'
2980 while True:
2981 if filename in os.listdir(cwd):
2982 if os.path.isfile(os.path.join(cwd, filename)):
2983 return open(os.path.join(cwd, filename))
2984 if cwd == root:
2985 break
2986 cwd = os.path.dirname(cwd)
2987
2988
2989def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002990 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002991 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002992
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002993 def SetProperty(name, setting, unset_error_ok=False):
2994 fullname = 'rietveld.' + name
2995 if setting in keyvals:
2996 RunGit(['config', fullname, keyvals[setting]])
2997 else:
2998 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2999
tandrii48df5812016-10-17 03:55:37 -07003000 if not keyvals.get('GERRIT_HOST', False):
3001 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003002 # Only server setting is required. Other settings can be absent.
3003 # In that case, we ignore errors raised during option deletion attempt.
3004 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3005 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3006 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003007 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003008 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3009 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003010 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3011 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003012 SetProperty(
3013 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003014
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003015 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003016 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003017
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003018 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003019 RunGit(['config', 'gerrit.squash-uploads',
3020 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003021
tandrii@chromium.org28253532016-04-14 13:46:56 +00003022 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003023 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003024 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003026 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003027 # should be of the form
3028 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3029 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003030 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3031 keyvals['ORIGIN_URL_CONFIG']])
3032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003034def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003035 """Downloads a network object to a local file, like urllib.urlretrieve.
3036
3037 This is necessary because urllib is broken for SSL connections via a proxy.
3038 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003039 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003040 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003041
3042
ukai@chromium.org712d6102013-11-27 00:52:58 +00003043def hasSheBang(fname):
3044 """Checks fname is a #! script."""
3045 with open(fname) as f:
3046 return f.read(2).startswith('#!')
3047
3048
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003049# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3050def DownloadHooks(*args, **kwargs):
3051 pass
3052
3053
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003054def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003055 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003056
3057 Args:
3058 force: True to update hooks. False to install hooks if not present.
3059 """
3060 if not settings.GetIsGerrit():
3061 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003062 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003063 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3064 if not os.access(dst, os.X_OK):
3065 if os.path.exists(dst):
3066 if not force:
3067 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003068 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003069 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003070 if not hasSheBang(dst):
3071 DieWithError('Not a script: %s\n'
3072 'You need to download from\n%s\n'
3073 'into .git/hooks/commit-msg and '
3074 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003075 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3076 except Exception:
3077 if os.path.exists(dst):
3078 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003079 DieWithError('\nFailed to download hooks.\n'
3080 'You need to download from\n%s\n'
3081 'into .git/hooks/commit-msg and '
3082 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003083
3084
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003085class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003086 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003087
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003088 _GOOGLESOURCE = 'googlesource.com'
3089
3090 def __init__(self):
3091 # Cached list of [host, identity, source], where source is either
3092 # .gitcookies or .netrc.
3093 self._all_hosts = None
3094
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003095 def ensure_configured_gitcookies(self):
3096 """Runs checks and suggests fixes to make git use .gitcookies from default
3097 path."""
3098 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3099 configured_path = RunGitSilent(
3100 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003101 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003102 if configured_path:
3103 self._ensure_default_gitcookies_path(configured_path, default)
3104 else:
3105 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003106
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003107 @staticmethod
3108 def _ensure_default_gitcookies_path(configured_path, default_path):
3109 assert configured_path
3110 if configured_path == default_path:
3111 print('git is already configured to use your .gitcookies from %s' %
3112 configured_path)
3113 return
3114
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003115 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003116 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3117 (configured_path, default_path))
3118
3119 if not os.path.exists(configured_path):
3120 print('However, your configured .gitcookies file is missing.')
3121 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3122 action='reconfigure')
3123 RunGit(['config', '--global', 'http.cookiefile', default_path])
3124 return
3125
3126 if os.path.exists(default_path):
3127 print('WARNING: default .gitcookies file already exists %s' %
3128 default_path)
3129 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3130 default_path)
3131
3132 confirm_or_exit('Move existing .gitcookies to default location?',
3133 action='move')
3134 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003135 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003136 print('Moved and reconfigured git to use .gitcookies from %s' %
3137 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003138
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003139 @staticmethod
3140 def _configure_gitcookies_path(default_path):
3141 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3142 if os.path.exists(netrc_path):
3143 print('You seem to be using outdated .netrc for git credentials: %s' %
3144 netrc_path)
3145 print('This tool will guide you through setting up recommended '
3146 '.gitcookies store for git credentials.\n'
3147 '\n'
3148 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3149 ' git config --global --unset http.cookiefile\n'
3150 ' mv %s %s.backup\n\n' % (default_path, default_path))
3151 confirm_or_exit(action='setup .gitcookies')
3152 RunGit(['config', '--global', 'http.cookiefile', default_path])
3153 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003154
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003155 def get_hosts_with_creds(self, include_netrc=False):
3156 if self._all_hosts is None:
3157 a = gerrit_util.CookiesAuthenticator()
3158 self._all_hosts = [
3159 (h, u, s)
3160 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003161 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
3162 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003163 )
3164 if h.endswith(self._GOOGLESOURCE)
3165 ]
3166
3167 if include_netrc:
3168 return self._all_hosts
3169 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3170
3171 def print_current_creds(self, include_netrc=False):
3172 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3173 if not hosts:
3174 print('No Git/Gerrit credentials found')
3175 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003176 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003177 header = [('Host', 'User', 'Which file'),
3178 ['=' * l for l in lengths]]
3179 for row in (header + hosts):
3180 print('\t'.join((('%%+%ds' % l) % s)
3181 for l, s in zip(lengths, row)))
3182
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003183 @staticmethod
3184 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003185 """Parses identity "git-<username>.domain" into <username> and domain."""
3186 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003187 # distinguishable from sub-domains. But we do know typical domains:
3188 if identity.endswith('.chromium.org'):
3189 domain = 'chromium.org'
3190 username = identity[:-len('.chromium.org')]
3191 else:
3192 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003193 if username.startswith('git-'):
3194 username = username[len('git-'):]
3195 return username, domain
3196
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003197 def _canonical_git_googlesource_host(self, host):
3198 """Normalizes Gerrit hosts (with '-review') to Git host."""
3199 assert host.endswith(self._GOOGLESOURCE)
3200 # Prefix doesn't include '.' at the end.
3201 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3202 if prefix.endswith('-review'):
3203 prefix = prefix[:-len('-review')]
3204 return prefix + '.' + self._GOOGLESOURCE
3205
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003206 def _canonical_gerrit_googlesource_host(self, host):
3207 git_host = self._canonical_git_googlesource_host(host)
3208 prefix = git_host.split('.', 1)[0]
3209 return prefix + '-review.' + self._GOOGLESOURCE
3210
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003211 def _get_counterpart_host(self, host):
3212 assert host.endswith(self._GOOGLESOURCE)
3213 git = self._canonical_git_googlesource_host(host)
3214 gerrit = self._canonical_gerrit_googlesource_host(git)
3215 return git if gerrit == host else gerrit
3216
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003217 def has_generic_host(self):
3218 """Returns whether generic .googlesource.com has been configured.
3219
3220 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3221 """
3222 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3223 if host == '.' + self._GOOGLESOURCE:
3224 return True
3225 return False
3226
3227 def _get_git_gerrit_identity_pairs(self):
3228 """Returns map from canonic host to pair of identities (Git, Gerrit).
3229
3230 One of identities might be None, meaning not configured.
3231 """
3232 host_to_identity_pairs = {}
3233 for host, identity, _ in self.get_hosts_with_creds():
3234 canonical = self._canonical_git_googlesource_host(host)
3235 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3236 idx = 0 if canonical == host else 1
3237 pair[idx] = identity
3238 return host_to_identity_pairs
3239
3240 def get_partially_configured_hosts(self):
3241 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003242 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003243 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003244 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003245
3246 def get_conflicting_hosts(self):
3247 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003248 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003249 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003250 if None not in (i1, i2) and i1 != i2)
3251
3252 def get_duplicated_hosts(self):
3253 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003254 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003255
3256 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3257 'chromium.googlesource.com': 'chromium.org',
3258 'chrome-internal.googlesource.com': 'google.com',
3259 }
3260
3261 def get_hosts_with_wrong_identities(self):
3262 """Finds hosts which **likely** reference wrong identities.
3263
3264 Note: skips hosts which have conflicting identities for Git and Gerrit.
3265 """
3266 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003267 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003268 pair = self._get_git_gerrit_identity_pairs().get(host)
3269 if pair and pair[0] == pair[1]:
3270 _, domain = self._parse_identity(pair[0])
3271 if domain != expected:
3272 hosts.add(host)
3273 return hosts
3274
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003275 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003276 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003277 hosts = sorted(hosts)
3278 assert hosts
3279 if extra_column_func is None:
3280 extras = [''] * len(hosts)
3281 else:
3282 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003283 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3284 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003285 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003286 lines.append(tmpl % he)
3287 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003288
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003289 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003290 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003291 yield ('.googlesource.com wildcard record detected',
3292 ['Chrome Infrastructure team recommends to list full host names '
3293 'explicitly.'],
3294 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003295
3296 dups = self.get_duplicated_hosts()
3297 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003298 yield ('The following hosts were defined twice',
3299 self._format_hosts(dups),
3300 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003301
3302 partial = self.get_partially_configured_hosts()
3303 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003304 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3305 'These hosts are missing',
3306 self._format_hosts(partial, lambda host: 'but %s defined' %
3307 self._get_counterpart_host(host)),
3308 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003309
3310 conflicting = self.get_conflicting_hosts()
3311 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003312 yield ('The following Git hosts have differing credentials from their '
3313 'Gerrit counterparts',
3314 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3315 tuple(self._get_git_gerrit_identity_pairs()[host])),
3316 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003317
3318 wrong = self.get_hosts_with_wrong_identities()
3319 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003320 yield ('These hosts likely use wrong identity',
3321 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3322 (self._get_git_gerrit_identity_pairs()[host][0],
3323 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3324 wrong)
3325
3326 def find_and_report_problems(self):
3327 """Returns True if there was at least one problem, else False."""
3328 found = False
3329 bad_hosts = set()
3330 for title, sublines, hosts in self._find_problems():
3331 if not found:
3332 found = True
3333 print('\n\n.gitcookies problem report:\n')
3334 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003335 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003336 if sublines:
3337 print()
3338 print(' %s' % '\n '.join(sublines))
3339 print()
3340
3341 if bad_hosts:
3342 assert found
3343 print(' You can manually remove corresponding lines in your %s file and '
3344 'visit the following URLs with correct account to generate '
3345 'correct credential lines:\n' %
3346 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3347 print(' %s' % '\n '.join(sorted(set(
3348 gerrit_util.CookiesAuthenticator().get_new_password_url(
3349 self._canonical_git_googlesource_host(host))
3350 for host in bad_hosts
3351 ))))
3352 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003353
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003354
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003355@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003356def CMDcreds_check(parser, args):
3357 """Checks credentials and suggests changes."""
3358 _, _ = parser.parse_args(args)
3359
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003360 # Code below checks .gitcookies. Abort if using something else.
3361 authn = gerrit_util.Authenticator.get()
3362 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003363 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003364 'This command is not designed for bot environment. It checks '
3365 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003366 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3367 if isinstance(authn, gerrit_util.GceAuthenticator):
3368 message += (
3369 '\n'
3370 'If you need to run this on GCE or a cloudtop instance, '
3371 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3372 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003373
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003374 checker = _GitCookiesChecker()
3375 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003376
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003377 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003378 checker.print_current_creds(include_netrc=True)
3379
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003380 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003381 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003382 return 0
3383 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003384
3385
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003386@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003387def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003388 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003389 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
Edward Lemur85153282020-02-14 22:06:29 +00003390 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003391 _, args = parser.parse_args(args)
3392 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003393 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003394 return RunGit(['config', 'branch.%s.base-url' % branch],
3395 error_ok=False).strip()
3396 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003397 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003398 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3399 error_ok=False).strip()
3400
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003401
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003402def color_for_status(status):
3403 """Maps a Changelist status to color, for CMDstatus and other tools."""
3404 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003405 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003406 'waiting': Fore.BLUE,
3407 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003408 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003409 'lgtm': Fore.GREEN,
3410 'commit': Fore.MAGENTA,
3411 'closed': Fore.CYAN,
3412 'error': Fore.WHITE,
3413 }.get(status, Fore.WHITE)
3414
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003415
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003416def get_cl_statuses(changes, fine_grained, max_processes=None):
3417 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003418
3419 If fine_grained is true, this will fetch CL statuses from the server.
3420 Otherwise, simply indicate if there's a matching url for the given branches.
3421
3422 If max_processes is specified, it is used as the maximum number of processes
3423 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3424 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003425
3426 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003427 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003428 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003429 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003430
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003431 if not fine_grained:
3432 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003433 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003434 for cl in changes:
3435 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003436 return
3437
3438 # First, sort out authentication issues.
3439 logging.debug('ensuring credentials exist')
3440 for cl in changes:
3441 cl.EnsureAuthenticated(force=False, refresh=True)
3442
3443 def fetch(cl):
3444 try:
3445 return (cl, cl.GetStatus())
3446 except:
3447 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003448 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003449 raise
3450
3451 threads_count = len(changes)
3452 if max_processes:
3453 threads_count = max(1, min(threads_count, max_processes))
3454 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3455
Edward Lemur61bf4172020-02-24 23:22:37 +00003456 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003457 fetched_cls = set()
3458 try:
3459 it = pool.imap_unordered(fetch, changes).__iter__()
3460 while True:
3461 try:
3462 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003463 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003464 break
3465 fetched_cls.add(cl)
3466 yield cl, status
3467 finally:
3468 pool.close()
3469
3470 # Add any branches that failed to fetch.
3471 for cl in set(changes) - fetched_cls:
3472 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003473
rmistry@google.com2dd99862015-06-22 12:22:18 +00003474
3475def upload_branch_deps(cl, args):
3476 """Uploads CLs of local branches that are dependents of the current branch.
3477
3478 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003479
3480 test1 -> test2.1 -> test3.1
3481 -> test3.2
3482 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003483
3484 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3485 run on the dependent branches in this order:
3486 test2.1, test3.1, test3.2, test2.2, test3.3
3487
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003488 Note: This function does not rebase your local dependent branches. Use it
3489 when you make a change to the parent branch that will not conflict
3490 with its dependent branches, and you would like their dependencies
3491 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003492 """
3493 if git_common.is_dirty_git_tree('upload-branch-deps'):
3494 return 1
3495
3496 root_branch = cl.GetBranch()
3497 if root_branch is None:
3498 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3499 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003500 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003501 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3502 'patchset dependencies without an uploaded CL.')
3503
3504 branches = RunGit(['for-each-ref',
3505 '--format=%(refname:short) %(upstream:short)',
3506 'refs/heads'])
3507 if not branches:
3508 print('No local branches found.')
3509 return 0
3510
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003511 # Create a dictionary of all local branches to the branches that are
3512 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003513 tracked_to_dependents = collections.defaultdict(list)
3514 for b in branches.splitlines():
3515 tokens = b.split()
3516 if len(tokens) == 2:
3517 branch_name, tracked = tokens
3518 tracked_to_dependents[tracked].append(branch_name)
3519
vapiera7fbd5a2016-06-16 09:17:49 -07003520 print()
3521 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003522 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003523
rmistry@google.com2dd99862015-06-22 12:22:18 +00003524 def traverse_dependents_preorder(branch, padding=''):
3525 dependents_to_process = tracked_to_dependents.get(branch, [])
3526 padding += ' '
3527 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003528 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003529 dependents.append(dependent)
3530 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003531
rmistry@google.com2dd99862015-06-22 12:22:18 +00003532 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003533 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003534
3535 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003536 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003537 return 0
3538
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003539 confirm_or_exit('This command will checkout all dependent branches and run '
3540 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003541
rmistry@google.com2dd99862015-06-22 12:22:18 +00003542 # Record all dependents that failed to upload.
3543 failures = {}
3544 # Go through all dependents, checkout the branch and upload.
3545 try:
3546 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003547 print()
3548 print('--------------------------------------')
3549 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003550 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003552 try:
3553 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003554 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003555 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003556 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003557 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003558 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003559 finally:
3560 # Swap back to the original root branch.
3561 RunGit(['checkout', '-q', root_branch])
3562
vapiera7fbd5a2016-06-16 09:17:49 -07003563 print()
3564 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003565 for dependent_branch in dependents:
3566 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003567 print(' %s : %s' % (dependent_branch, upload_status))
3568 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003569
3570 return 0
3571
3572
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003573def GetArchiveTagForBranch(issue_num, branch_name, existing_tags):
3574 """Given a proposed tag name, returns a tag name that is guaranteed to be
3575 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3576 or 'foo-3', and so on."""
3577
3578 proposed_tag = 'git-cl-archived-%s-%s' % (issue_num, branch_name)
3579 for suffix_num in itertools.count(1):
3580 if suffix_num == 1:
3581 to_check = proposed_tag
3582 else:
3583 to_check = '%s-%d' % (proposed_tag, suffix_num)
3584
3585 if to_check not in existing_tags:
3586 return to_check
3587
3588
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003589@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003590def CMDarchive(parser, args):
3591 """Archives and deletes branches associated with closed changelists."""
3592 parser.add_option(
3593 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003594 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003595 parser.add_option(
3596 '-f', '--force', action='store_true',
3597 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003598 parser.add_option(
3599 '-d', '--dry-run', action='store_true',
3600 help='Skip the branch tagging and removal steps.')
3601 parser.add_option(
3602 '-t', '--notags', action='store_true',
3603 help='Do not tag archived branches. '
3604 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003605
kmarshall3bff56b2016-06-06 18:31:47 -07003606 options, args = parser.parse_args(args)
3607 if args:
3608 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003609
3610 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3611 if not branches:
3612 return 0
3613
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003614 tags = RunGit(['for-each-ref', '--format=%(refname)',
3615 'refs/tags']).splitlines() or []
3616 tags = [t.split('/')[-1] for t in tags]
3617
vapiera7fbd5a2016-06-16 09:17:49 -07003618 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003619 changes = [Changelist(branchref=b)
3620 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003621 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3622 statuses = get_cl_statuses(changes,
3623 fine_grained=True,
3624 max_processes=options.maxjobs)
3625 proposal = [(cl.GetBranch(),
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003626 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(),
3627 tags))
kmarshall3bff56b2016-06-06 18:31:47 -07003628 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003629 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003630 proposal.sort()
3631
3632 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003633 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003634 return 0
3635
Edward Lemur85153282020-02-14 22:06:29 +00003636 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003637
vapiera7fbd5a2016-06-16 09:17:49 -07003638 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003639 if options.notags:
3640 for next_item in proposal:
3641 print(' ' + next_item[0])
3642 else:
3643 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3644 for next_item in proposal:
3645 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003646
kmarshall9249e012016-08-23 12:02:16 -07003647 # Quit now on precondition failure or if instructed by the user, either
3648 # via an interactive prompt or by command line flags.
3649 if options.dry_run:
3650 print('\nNo changes were made (dry run).\n')
3651 return 0
3652 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003653 print('You are currently on a branch \'%s\' which is associated with a '
3654 'closed codereview issue, so archive cannot proceed. Please '
3655 'checkout another branch and run this command again.' %
3656 current_branch)
3657 return 1
kmarshall9249e012016-08-23 12:02:16 -07003658 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003659 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3660 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003661 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003662 return 1
3663
3664 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003665 if not options.notags:
3666 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003667
3668 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3669 # Clean up the tag if we failed to delete the branch.
3670 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003671
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003673
3674 return 0
3675
3676
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003677@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003678def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003679 """Show status of changelists.
3680
3681 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003682 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003683 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003684 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003685 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003686 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003687 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003688 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003689
3690 Also see 'git cl comments'.
3691 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003692 parser.add_option(
3693 '--no-branch-color',
3694 action='store_true',
3695 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003696 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003697 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003698 parser.add_option('-f', '--fast', action='store_true',
3699 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003700 parser.add_option(
3701 '-j', '--maxjobs', action='store', type=int,
3702 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00003703 parser.add_option(
3704 '-i', '--issue', type=int,
3705 help='Operate on this issue instead of the current branch\'s implicit '
3706 'issue. Requires --field to be set.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003707 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003708 if args:
3709 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003710
iannuccie53c9352016-08-17 14:40:40 -07003711 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00003712 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07003713
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003714 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003715 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003716 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00003717 if cl.GetIssue():
3718 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 elif options.field == 'id':
3720 issueid = cl.GetIssue()
3721 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003722 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003724 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003727 elif options.field == 'status':
3728 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003729 elif options.field == 'url':
3730 url = cl.GetIssueURL()
3731 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003732 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003733 return 0
3734
3735 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3736 if not branches:
3737 print('No local branch found.')
3738 return 0
3739
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003740 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003741 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003742 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003744 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003745 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003746 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003747
Edward Lemur85153282020-02-14 22:06:29 +00003748 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00003749
3750 def FormatBranchName(branch, colorize=False):
3751 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3752 an asterisk when it is the current branch."""
3753
3754 asterisk = ""
3755 color = Fore.RESET
3756 if branch == current_branch:
3757 asterisk = "* "
3758 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00003759 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00003760
3761 if colorize:
3762 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003763 return asterisk + branch_name
3764
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003765 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003766
3767 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003768 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3769 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003770 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003771 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003772 branch_statuses[c.GetBranch()] = status
3773 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00003774 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003775 if url and (not status or status == 'error'):
3776 # The issue probably doesn't exist anymore.
3777 url += ' (broken)'
3778
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003779 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003780 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003781 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003782 color = ''
3783 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003784 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003785
Alan Cuttera3be9a52019-03-04 18:50:33 +00003786 branch_display = FormatBranchName(branch)
3787 padding = ' ' * (alignment - len(branch_display))
3788 if not options.no_branch_color:
3789 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003790
Alan Cuttera3be9a52019-03-04 18:50:33 +00003791 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3792 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003793
vapiera7fbd5a2016-06-16 09:17:49 -07003794 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003795 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003796 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003797 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003798 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003799 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003800 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003801 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003803 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003804 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00003805 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 return 0
3807
3808
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003809def colorize_CMDstatus_doc():
3810 """To be called once in main() to add colors to git cl status help."""
3811 colors = [i for i in dir(Fore) if i[0].isupper()]
3812
3813 def colorize_line(line):
3814 for color in colors:
3815 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003816 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003817 indent = len(line) - len(line.lstrip(' ')) + 1
3818 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3819 return line
3820
3821 lines = CMDstatus.__doc__.splitlines()
3822 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3823
3824
phajdan.jre328cf92016-08-22 04:12:17 -07003825def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003826 if path == '-':
3827 json.dump(contents, sys.stdout)
3828 else:
3829 with open(path, 'w') as f:
3830 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003831
3832
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003833@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003834@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003835def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003836 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837
3838 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003839 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003840 parser.add_option('-r', '--reverse', action='store_true',
3841 help='Lookup the branch(es) for the specified issues. If '
3842 'no issues are specified, all branches with mapped '
3843 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003844 parser.add_option('--json',
3845 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003846 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003847
dnj@chromium.org406c4402015-03-03 17:22:28 +00003848 if options.reverse:
3849 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003850 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003851 # Reverse issue lookup.
3852 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003853
3854 git_config = {}
3855 for config in RunGit(['config', '--get-regexp',
3856 r'branch\..*issue']).splitlines():
3857 name, _space, val = config.partition(' ')
3858 git_config[name] = val
3859
dnj@chromium.org406c4402015-03-03 17:22:28 +00003860 for branch in branches:
Edward Lemur85153282020-02-14 22:06:29 +00003861 config_key = _git_branch_config_key(scm.GIT.ShortBranchName(branch),
Edward Lemur52969c92020-02-06 18:15:28 +00003862 Changelist.IssueConfigKey())
3863 issue = git_config.get(config_key)
3864 if issue:
3865 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003866 if not args:
3867 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003868 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003869 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003870 try:
3871 issue_num = int(issue)
3872 except ValueError:
3873 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003874 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003875 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003876 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003877 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003878 if options.json:
3879 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003880 return 0
3881
3882 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003883 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003884 if not issue.valid:
3885 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3886 'or no argument to list it.\n'
3887 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003888 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003889 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003890 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003891 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003892 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
3893 if options.json:
3894 write_json(options.json, {
3895 'issue': cl.GetIssue(),
3896 'issue_url': cl.GetIssueURL(),
3897 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003898 return 0
3899
3900
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003901@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003902def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003903 """Shows or posts review comments for any changelist."""
3904 parser.add_option('-a', '--add-comment', dest='comment',
3905 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003906 parser.add_option('-p', '--publish', action='store_true',
3907 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01003908 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00003909 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003910 parser.add_option('-m', '--machine-readable', dest='readable',
3911 action='store_false', default=True,
3912 help='output comments in a format compatible with '
3913 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00003914 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07003915 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003916 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003917
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003918 issue = None
3919 if options.issue:
3920 try:
3921 issue = int(options.issue)
3922 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003923 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003924
Edward Lemur934836a2019-09-09 20:16:54 +00003925 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003926
3927 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00003928 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003929 return 0
3930
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07003931 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
3932 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003933 for comment in summary:
3934 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003935 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003936 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003937 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003938 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003939 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00003940 elif comment.autogenerated:
3941 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003942 else:
3943 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003944 print('\n%s%s %s%s\n%s' % (
3945 color,
3946 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
3947 comment.sender,
3948 Fore.RESET,
3949 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
3950
smut@google.comc85ac942015-09-15 16:34:43 +00003951 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003952 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00003953 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01003954 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
3955 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00003956 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003957 return 0
3958
3959
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003960@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003961@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003962def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003963 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003964 parser.add_option('-d', '--display', action='store_true',
3965 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003966 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003967 help='New description to set for this issue (- for stdin, '
3968 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003969 parser.add_option('-f', '--force', action='store_true',
3970 help='Delete any unpublished Gerrit edits for this issue '
3971 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003972
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003973 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003974
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003975 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003976 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003977 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003978 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003979 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003980
Edward Lemur934836a2019-09-09 20:16:54 +00003981 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01003982 if target_issue_arg:
3983 kwargs['issue'] = target_issue_arg.issue
3984 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07003985
3986 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003987 if not cl.GetIssue():
3988 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003989
Edward Lemur678a6842019-10-03 22:25:05 +00003990 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00003991 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02003992
Edward Lemur6c6827c2020-02-06 21:15:18 +00003993 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003994
smut@google.com34fb6b12015-07-13 20:03:26 +00003995 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003996 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003997 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003998
3999 if options.new_description:
4000 text = options.new_description
4001 if text == '-':
4002 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004003 elif text == '+':
4004 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00004005 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004006
4007 description.set_description(text)
4008 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004009 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00004010 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004011 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004012 return 0
4013
4014
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004015@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004016def CMDlint(parser, args):
4017 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004018 parser.add_option('--filter', action='append', metavar='-x,+y',
4019 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004020 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004021
4022 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004023 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004024 try:
4025 import cpplint
4026 import cpplint_chromium
4027 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004028 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004029 return 1
4030
4031 # Change the current working directory before calling lint so that it
4032 # shows the correct base.
4033 previous_cwd = os.getcwd()
4034 os.chdir(settings.GetRoot())
4035 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004036 cl = Changelist()
Edward Lemura12175c2020-03-09 16:58:26 +00004037 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), '')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004038 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004039 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004040 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004041 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004042
4043 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004044 command = args + files
4045 if options.filter:
4046 command = ['--filter=' + ','.join(options.filter)] + command
4047 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004048
4049 white_regex = re.compile(settings.GetLintRegex())
4050 black_regex = re.compile(settings.GetLintIgnoreRegex())
4051 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4052 for filename in filenames:
4053 if white_regex.match(filename):
4054 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004055 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004056 else:
4057 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4058 extra_check_functions)
4059 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004060 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004061 finally:
4062 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004063 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004064 if cpplint._cpplint_state.error_count != 0:
4065 return 1
4066 return 0
4067
4068
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004069@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004070def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004071 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004072 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004073 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004074 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004075 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004076 parser.add_option('--all', action='store_true',
4077 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004078 parser.add_option('--parallel', action='store_true',
4079 help='Run all tests specified by input_api.RunTests in all '
4080 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004081 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082
sbc@chromium.org71437c02015-04-09 19:29:40 +00004083 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004084 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004085 return 1
4086
Edward Lemur934836a2019-09-09 20:16:54 +00004087 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088 if args:
4089 base_branch = args[0]
4090 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004091 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004092 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093
Gregory Nisbet29d5cf82020-02-27 08:16:58 +00004094 if cl.GetIssue():
4095 description = cl.FetchDescription()
Aaron Gable8076c282017-11-29 14:39:41 -08004096 else:
Edward Lemura12175c2020-03-09 16:58:26 +00004097 description = _create_description_from_log([base_branch])
Aaron Gable8076c282017-11-29 14:39:41 -08004098
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004099 cl.RunHook(
4100 committing=not options.upload,
4101 may_prompt=False,
4102 verbose=options.verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00004103 parallel=options.parallel,
4104 upstream=base_branch,
4105 description=description,
4106 all_files=options.all)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004107 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108
4109
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004110def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004111 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004112
4113 Works the same way as
4114 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4115 but can be called on demand on all platforms.
4116
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004117 The basic idea is to generate git hash of a state of the tree, original
4118 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004119 """
4120 lines = []
4121 tree_hash = RunGitSilent(['write-tree'])
4122 lines.append('tree %s' % tree_hash.strip())
4123 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4124 if code == 0:
4125 lines.append('parent %s' % parent.strip())
4126 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4127 lines.append('author %s' % author.strip())
4128 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4129 lines.append('committer %s' % committer.strip())
4130 lines.append('')
4131 # Note: Gerrit's commit-hook actually cleans message of some lines and
4132 # whitespace. This code is not doing this, but it clearly won't decrease
4133 # entropy.
4134 lines.append(message)
4135 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004136 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004137 return 'I%s' % change_hash.strip()
4138
4139
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004140def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004141 """Computes the remote branch ref to use for the CL.
4142
4143 Args:
4144 remote (str): The git remote for the CL.
4145 remote_branch (str): The git remote branch for the CL.
4146 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004147 """
4148 if not (remote and remote_branch):
4149 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004150
wittman@chromium.org455dc922015-01-26 20:15:50 +00004151 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004152 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004153 # refs, which are then translated into the remote full symbolic refs
4154 # below.
4155 if '/' not in target_branch:
4156 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4157 else:
4158 prefix_replacements = (
4159 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4160 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4161 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4162 )
4163 match = None
4164 for regex, replacement in prefix_replacements:
4165 match = re.search(regex, target_branch)
4166 if match:
4167 remote_branch = target_branch.replace(match.group(0), replacement)
4168 break
4169 if not match:
4170 # This is a branch path but not one we recognize; use as-is.
4171 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004172 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4173 # Handle the refs that need to land in different refs.
4174 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004175
wittman@chromium.org455dc922015-01-26 20:15:50 +00004176 # Create the true path to the remote branch.
4177 # Does the following translation:
4178 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4179 # * refs/remotes/origin/master -> refs/heads/master
4180 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4181 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4182 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4183 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4184 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4185 'refs/heads/')
4186 elif remote_branch.startswith('refs/remotes/branch-heads'):
4187 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004188
wittman@chromium.org455dc922015-01-26 20:15:50 +00004189 return remote_branch
4190
4191
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004192def cleanup_list(l):
4193 """Fixes a list so that comma separated items are put as individual items.
4194
4195 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4196 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4197 """
4198 items = sum((i.split(',') for i in l), [])
4199 stripped_items = (i.strip() for i in items)
4200 return sorted(filter(None, stripped_items))
4201
4202
Aaron Gable4db38df2017-11-03 14:59:07 -07004203@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004204@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004205def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004206 """Uploads the current changelist to codereview.
4207
4208 Can skip dependency patchset uploads for a branch by running:
4209 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004210 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004211 git config --unset branch.branch_name.skip-deps-uploads
4212 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004213
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004214 If the name of the checked out branch starts with "bug-" or "fix-" followed
4215 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004216 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004217
4218 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004219 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004220 [git-cl] add support for hashtags
4221 Foo bar: implement foo
4222 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004223 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004224 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4225 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004226 parser.add_option('--bypass-watchlists', action='store_true',
4227 dest='bypass_watchlists',
4228 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004229 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004230 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004231 parser.add_option('--message', '-m', dest='message',
4232 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004233 parser.add_option('-b', '--bug',
4234 help='pre-populate the bug number(s) for this issue. '
4235 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004236 parser.add_option('--message-file', dest='message_file',
4237 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004238 parser.add_option('--title', '-t', dest='title',
4239 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004240 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004241 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004242 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004243 parser.add_option('--tbrs',
4244 action='append', default=[],
4245 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004246 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004247 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004248 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004249 parser.add_option('--hashtag', dest='hashtags',
4250 action='append', default=[],
4251 help=('Gerrit hashtag for new CL; '
4252 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004253 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004254 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004255 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004256 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004257 metavar='TARGET',
4258 help='Apply CL to remote ref TARGET. ' +
4259 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004260 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004261 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004262 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004263 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004264 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004265 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004266 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4267 const='TBR', help='add a set of OWNERS to TBR')
4268 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4269 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004270 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004271 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004272 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004273 'implies --send-mail')
4274 parser.add_option('-d', '--cq-dry-run',
4275 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004276 help='Send the patchset to do a CQ dry run right after '
4277 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004278 parser.add_option('--preserve-tryjobs', action='store_true',
4279 help='instruct the CQ to let tryjobs running even after '
4280 'new patchsets are uploaded instead of canceling '
4281 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004282 parser.add_option('--dependencies', action='store_true',
4283 help='Uploads CLs of all the local branches that depend on '
4284 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004285 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4286 help='Sends your change to the CQ after an approval. Only '
4287 'works on repos that have the Auto-Submit label '
4288 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004289 parser.add_option('--parallel', action='store_true',
4290 help='Run all tests specified by input_api.RunTests in all '
4291 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004292 parser.add_option('--no-autocc', action='store_true',
4293 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004294 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004295 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004296 parser.add_option('-R', '--retry-failed', action='store_true',
4297 help='Retry failed tryjobs from old patchset immediately '
4298 'after uploading new patchset. Cannot be used with '
4299 '--use-commit-queue or --cq-dry-run.')
4300 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4301 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004302 parser.add_option('--fixed', '-x',
4303 help='List of bugs that will be commented on and marked '
4304 'fixed (pre-populates "Fixed:" tag). Same format as '
4305 '-b option / "Bug:" tag. If fixing several issues, '
4306 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004307 parser.add_option('--edit-description', action='store_true', default=False,
4308 help='Modify description before upload. Cannot be used '
4309 'with --force. It is a noop when --no-squash is set '
4310 'or a new commit is created.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004311
rmistry@google.com2dd99862015-06-22 12:22:18 +00004312 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004313 (options, args) = parser.parse_args(args)
4314
sbc@chromium.org71437c02015-04-09 19:29:40 +00004315 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004316 return 1
4317
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004318 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004319 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004320 options.cc = cleanup_list(options.cc)
4321
Josipe827b0f2020-01-30 00:07:20 +00004322 if options.edit_description and options.force:
4323 parser.error('Only one of --force and --edit-description allowed')
4324
tandriib80458a2016-06-23 12:20:07 -07004325 if options.message_file:
4326 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004327 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004328 options.message = gclient_utils.FileRead(options.message_file)
4329 options.message_file = None
4330
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004331 if ([options.cq_dry_run,
4332 options.use_commit_queue,
4333 options.retry_failed].count(True) > 1):
4334 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4335 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004336
Aaron Gableedbc4132017-09-11 13:22:28 -07004337 if options.use_commit_queue:
4338 options.send_mail = True
4339
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004340 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4341 settings.GetIsGerrit()
4342
Edward Lemur934836a2019-09-09 20:16:54 +00004343 cl = Changelist()
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004344 # Warm change details cache now to avoid RPCs later, reducing latency for
4345 # developers.
4346 if cl.GetIssue():
4347 cl._GetChangeDetail(
4348 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4349
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004350 if options.retry_failed and not cl.GetIssue():
4351 print('No previous patchsets, so --retry-failed has no effect.')
4352 options.retry_failed = False
Edward Lesmes7677e5c2020-02-19 20:39:03 +00004353
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004354 # cl.GetMostRecentPatchset uses cached information, and can return the last
4355 # patchset before upload. Calling it here makes it clear that it's the
4356 # last patchset before upload. Note that GetMostRecentPatchset will fail
4357 # if no CL has been uploaded yet.
4358 if options.retry_failed:
4359 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004360
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004361 ret = cl.CMDUpload(options, args, orig_args)
4362
4363 if options.retry_failed:
4364 if ret != 0:
4365 print('Upload failed, so --retry-failed has no effect.')
4366 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004367 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004368 cl, options.buildbucket_host, latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004369 jobs = _filter_failed_for_retry(builds)
4370 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004371 print('No failed tryjobs, so --retry-failed has no effect.')
4372 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004373 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004374
4375 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004376
4377
Francois Dorayd42c6812017-05-30 15:10:20 -04004378@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004379@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004380def CMDsplit(parser, args):
4381 """Splits a branch into smaller branches and uploads CLs.
4382
4383 Creates a branch and uploads a CL for each group of files modified in the
4384 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00004385 comment, the string '$directory', is replaced with the directory containing
4386 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04004387 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004388 parser.add_option('-d', '--description', dest='description_file',
4389 help='A text file containing a CL description in which '
4390 '$directory will be replaced by each CL\'s directory.')
4391 parser.add_option('-c', '--comment', dest='comment_file',
4392 help='A text file containing a CL comment.')
4393 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004394 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004395 help='List the files and reviewers for each CL that would '
4396 'be created, but don\'t create branches or CLs.')
4397 parser.add_option('--cq-dry-run', action='store_true',
4398 help='If set, will do a cq dry run for each uploaded CL. '
4399 'Please be careful when doing this; more than ~10 CLs '
4400 'has the potential to overload our build '
4401 'infrastructure. Try to upload these not during high '
4402 'load times (usually 11-3 Mountain View time). Email '
4403 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004404 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4405 default=True,
4406 help='Sends your change to the CQ after an approval. Only '
4407 'works on repos that have the Auto-Submit label '
4408 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004409 options, _ = parser.parse_args(args)
4410
4411 if not options.description_file:
4412 parser.error('No --description flag specified.')
4413
4414 def WrappedCMDupload(args):
4415 return CMDupload(OptionParser(), args)
4416
4417 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004418 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004419 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004420
4421
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004422@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004423@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004424def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004425 """DEPRECATED: Used to commit the current changelist via git-svn."""
4426 message = ('git-cl no longer supports committing to SVN repositories via '
4427 'git-svn. You probably want to use `git cl land` instead.')
4428 print(message)
4429 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430
4431
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004432@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004433@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004434def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004435 """Commits the current changelist via git.
4436
4437 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4438 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004439 """
4440 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4441 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004442 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004443 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004444 parser.add_option('--parallel', action='store_true',
4445 help='Run all tests specified by input_api.RunTests in all '
4446 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004447 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004448
Edward Lemur934836a2019-09-09 20:16:54 +00004449 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004450
Robert Iannucci2e73d432018-03-14 01:10:47 -07004451 if not cl.GetIssue():
4452 DieWithError('You must upload the change first to Gerrit.\n'
4453 ' If you would rather have `git cl land` upload '
4454 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004455 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004456 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004457
4458
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004459@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004460@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004461def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004462 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004463 parser.add_option('-b', dest='newbranch',
4464 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004465 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004466 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004468 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004469
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004470 group = optparse.OptionGroup(
4471 parser,
4472 'Options for continuing work on the current issue uploaded from a '
4473 'different clone (e.g. different machine). Must be used independently '
4474 'from the other options. No issue number should be specified, and the '
4475 'branch must have an issue number associated with it')
4476 group.add_option('--reapply', action='store_true', dest='reapply',
4477 help='Reset the branch and reapply the issue.\n'
4478 'CAUTION: This will undo any local changes in this '
4479 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004480
4481 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004482 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004483 parser.add_option_group(group)
4484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004485 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004486
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004487 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004488 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004489 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004490 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004491 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004492
Edward Lemur934836a2019-09-09 20:16:54 +00004493 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004494 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004495 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004496
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004497 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004498 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004499 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004500
4501 RunGit(['reset', '--hard', upstream])
4502 if options.pull:
4503 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004504
Edward Lemur678a6842019-10-03 22:25:05 +00004505 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4506 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004507
4508 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004509 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004510
Edward Lemurf38bc172019-09-03 21:02:13 +00004511 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004512 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004513 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004514
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004515 # We don't want uncommitted changes mixed up with the patch.
4516 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004517 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004518
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004519 if options.newbranch:
4520 if options.force:
4521 RunGit(['branch', '-D', options.newbranch],
4522 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00004523 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004524
Edward Lemur678a6842019-10-03 22:25:05 +00004525 cl = Changelist(
4526 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004527
Edward Lemur678a6842019-10-03 22:25:05 +00004528 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004529 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004530
Edward Lemurf38bc172019-09-03 21:02:13 +00004531 return cl.CMDPatchWithParsedIssue(
4532 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533
4534
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004535def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536 """Fetches the tree status and returns either 'open', 'closed',
4537 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004538 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004539 if url:
Edward Lemur79d4f992019-11-11 23:49:02 +00004540 status = urllib.request.urlopen(url).read().lower()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541 if status.find('closed') != -1 or status == '0':
4542 return 'closed'
4543 elif status.find('open') != -1 or status == '1':
4544 return 'open'
4545 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004546 return 'unset'
4547
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004548
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004549def GetTreeStatusReason():
4550 """Fetches the tree status from a json url and returns the message
4551 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004552 url = settings.GetTreeStatusUrl()
4553 json_url = urlparse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004554 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004555 status = json.loads(connection.read())
4556 connection.close()
4557 return status['message']
4558
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004559
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004560@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004562 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004563 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004564 status = GetTreeStatus()
4565 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004566 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567 return 2
4568
vapiera7fbd5a2016-06-16 09:17:49 -07004569 print('The tree is %s' % status)
4570 print()
4571 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004572 if status != 'open':
4573 return 1
4574 return 0
4575
4576
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004577@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004578def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004579 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4580 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004581 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004582 '-b', '--bot', action='append',
4583 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4584 'times to specify multiple builders. ex: '
4585 '"-b win_rel -b win_layout". See '
4586 'the try server waterfall for the builders name and the tests '
4587 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004588 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004589 '-B', '--bucket', default='',
4590 help=('Buildbucket bucket to send the try requests.'))
4591 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004592 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004593 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004594 'be determined by the try recipe that builder runs, which usually '
4595 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004596 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004597 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004598 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004599 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004600 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004601 '--category', default='git_cl_try', help='Specify custom build category.')
4602 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004603 '--project',
4604 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004605 'in recipe to determine to which repository or directory to '
4606 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004607 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004608 '-p', '--property', dest='properties', action='append', default=[],
4609 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004610 'key2=value2 etc. The value will be treated as '
4611 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004612 'NOTE: using this may make your tryjob not usable for CQ, '
4613 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004614 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004615 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4616 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004617 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004618 parser.add_option(
4619 '-R', '--retry-failed', action='store_true', default=False,
4620 help='Retry failed jobs from the latest set of tryjobs. '
4621 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00004622 parser.add_option(
4623 '-i', '--issue', type=int,
4624 help='Operate on this issue instead of the current branch\'s implicit '
4625 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004626 options, args = parser.parse_args(args)
4627
machenbach@chromium.org45453142015-09-15 08:45:22 +00004628 # Make sure that all properties are prop=value pairs.
4629 bad_params = [x for x in options.properties if '=' not in x]
4630 if bad_params:
4631 parser.error('Got properties with missing "=": %s' % bad_params)
4632
maruel@chromium.org15192402012-09-06 12:38:29 +00004633 if args:
4634 parser.error('Unknown arguments: %s' % args)
4635
Edward Lemur934836a2019-09-09 20:16:54 +00004636 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004637 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004638 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004639
Edward Lemurf38bc172019-09-03 21:02:13 +00004640 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004641 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004642
tandriie113dfd2016-10-11 10:20:12 -07004643 error_message = cl.CannotTriggerTryJobReason()
4644 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004645 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004646
Edward Lemur45768512020-03-02 19:03:14 +00004647 if options.bot:
4648 if options.retry_failed:
4649 parser.error('--bot is not compatible with --retry-failed.')
4650 if not options.bucket:
4651 parser.error('A bucket (e.g. "chromium/try") is required.')
4652
4653 triggered = [b for b in options.bot if 'triggered' in b]
4654 if triggered:
4655 parser.error(
4656 'Cannot schedule builds on triggered bots: %s.\n'
4657 'This type of bot requires an initial job from a parent (usually a '
4658 'builder). Schedule a job on the parent instead.\n' % triggered)
4659
4660 if options.bucket.startswith('.master'):
4661 parser.error('Buildbot masters are not supported.')
4662
4663 project, bucket = _parse_bucket(options.bucket)
4664 if project is None or bucket is None:
4665 parser.error('Invalid bucket: %s.' % options.bucket)
4666 jobs = sorted((project, bucket, bot) for bot in options.bot)
4667 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004668 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004669 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004670 if options.verbose:
4671 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00004672 jobs = _filter_failed_for_retry(builds)
4673 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00004674 print('There are no failed jobs in the latest set of jobs '
4675 '(patchset #%d), doing nothing.' % patchset)
4676 return 0
Edward Lemur45768512020-03-02 19:03:14 +00004677 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004678 if num_builders > 10:
4679 confirm_or_exit('There are %d builders with failed builds.'
4680 % num_builders, action='continue')
4681 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07004682 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004683 print('git cl try with no bots now defaults to CQ dry run.')
4684 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4685 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004686
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004687 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004688 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004689 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004690 except BuildbucketResponseException as ex:
4691 print('ERROR: %s' % ex)
4692 return 1
4693 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004694
4695
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004696@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004697def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004698 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004699 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004700 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004701 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004702 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004703 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004704 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004705 '--color', action='store_true', default=setup_color.IS_TTY,
4706 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004707 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004708 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4709 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004710 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004711 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004712 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004713 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00004714 parser.add_option(
4715 '-i', '--issue', type=int,
4716 help='Operate on this issue instead of the current branch\'s implicit '
4717 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004718 options, args = parser.parse_args(args)
4719 if args:
4720 parser.error('Unrecognized args: %s' % ' '.join(args))
4721
Edward Lemur934836a2019-09-09 20:16:54 +00004722 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004723 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004724 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004725
tandrii221ab252016-10-06 08:12:04 -07004726 patchset = options.patchset
4727 if not patchset:
4728 patchset = cl.GetMostRecentPatchset()
4729 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004730 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004731 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004732 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004733 cl.GetIssue())
4734
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004735 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004736 jobs = _fetch_tryjobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004737 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004738 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004739 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004740 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004741 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004742 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00004743 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004744 return 0
4745
4746
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004747@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004748@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004749def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004750 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004751 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004752 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004753 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004754
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004755 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004756 if args:
4757 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004758 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004759 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004760 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004761 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004762
4763 # Clear configured merge-base, if there is one.
4764 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004765 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004766 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004767 return 0
4768
4769
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004770@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004771def CMDweb(parser, args):
4772 """Opens the current CL in the web browser."""
4773 _, args = parser.parse_args(args)
4774 if args:
4775 parser.error('Unrecognized args: %s' % ' '.join(args))
4776
4777 issue_url = Changelist().GetIssueURL()
4778 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004779 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004780 return 1
4781
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004782 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004783 # allows us to hide the "Created new window in existing browser session."
4784 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004785 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004786 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004787 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004788 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004789 os.open(os.devnull, os.O_RDWR)
4790 try:
4791 webbrowser.open(issue_url)
4792 finally:
4793 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004794 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004795 return 0
4796
4797
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004798@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004799def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004800 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004801 parser.add_option('-d', '--dry-run', action='store_true',
4802 help='trigger in dry run mode')
4803 parser.add_option('-c', '--clear', action='store_true',
4804 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00004805 parser.add_option(
4806 '-i', '--issue', type=int,
4807 help='Operate on this issue instead of the current branch\'s implicit '
4808 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004809 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004810 if args:
4811 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004812 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004813 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004814
Edward Lemur934836a2019-09-09 20:16:54 +00004815 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004816 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004817 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004818 elif options.dry_run:
4819 state = _CQState.DRY_RUN
4820 else:
4821 state = _CQState.COMMIT
4822 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004823 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004824 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004825 return 0
4826
4827
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004828@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004829def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004830 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00004831 parser.add_option(
4832 '-i', '--issue', type=int,
4833 help='Operate on this issue instead of the current branch\'s implicit '
4834 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004835 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004836 if args:
4837 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004838 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004839 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004840 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004841 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004842 cl.CloseIssue()
4843 return 0
4844
4845
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004846@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004847def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004848 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004849 parser.add_option(
4850 '--stat',
4851 action='store_true',
4852 dest='stat',
4853 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004854 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004855 if args:
4856 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004857
Edward Lemur934836a2019-09-09 20:16:54 +00004858 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004859 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004860 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004861 if not issue:
4862 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004863
Aaron Gablea718c3e2017-08-28 17:47:28 -07004864 base = cl._GitGetBranchConfigValue('last-upload-hash')
4865 if not base:
4866 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4867 if not base:
4868 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4869 revision_info = detail['revisions'][detail['current_revision']]
4870 fetch_info = revision_info['fetch']['http']
4871 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4872 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004873
Aaron Gablea718c3e2017-08-28 17:47:28 -07004874 cmd = ['git', 'diff']
4875 if options.stat:
4876 cmd.append('--stat')
4877 cmd.append(base)
4878 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004879
4880 return 0
4881
4882
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004883@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004884def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004885 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004886 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004887 '--ignore-current',
4888 action='store_true',
4889 help='Ignore the CL\'s current reviewers and start from scratch.')
4890 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004891 '--ignore-self',
4892 action='store_true',
4893 help='Do not consider CL\'s author as an owners.')
4894 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004895 '--no-color',
4896 action='store_true',
4897 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07004898 parser.add_option(
4899 '--batch',
4900 action='store_true',
4901 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00004902 # TODO: Consider moving this to another command, since other
4903 # git-cl owners commands deal with owners for a given CL.
4904 parser.add_option(
4905 '--show-all',
4906 action='store_true',
4907 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004908 options, args = parser.parse_args(args)
4909
4910 author = RunGit(['config', 'user.email']).strip() or None
4911
Edward Lemur934836a2019-09-09 20:16:54 +00004912 cl = Changelist()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004913
Yang Guo6e269a02019-06-26 11:17:02 +00004914 if options.show_all:
4915 for arg in args:
4916 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemurb7f759f2020-03-04 21:20:56 +00004917 database = owners.Database(settings.GetRoot(), open, os.path)
Yang Guo6e269a02019-06-26 11:17:02 +00004918 database.load_data_needed_for([arg])
4919 print('Owners for %s:' % arg)
4920 for owner in sorted(database.all_possible_owners([arg], None)):
4921 print(' - %s' % owner)
4922 return 0
4923
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004924 if args:
4925 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004926 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004927 base_branch = args[0]
4928 else:
4929 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004930 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004931
Edward Lemura12175c2020-03-09 16:58:26 +00004932 change = cl.GetChange(base_branch, '')
Dirk Prankebf980882017-09-02 15:08:00 -07004933 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
4934
4935 if options.batch:
Edward Lemurb7f759f2020-03-04 21:20:56 +00004936 db = owners.Database(change.RepositoryRoot(), open, os.path)
Dirk Prankebf980882017-09-02 15:08:00 -07004937 print('\n'.join(db.reviewers_for(affected_files, author)))
4938 return 0
4939
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004940 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07004941 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02004942 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01004943 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004944 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemurb7f759f2020-03-04 21:20:56 +00004945 fopen=open, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02004946 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004947 override_files=change.OriginalOwnersFiles(),
4948 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004949
4950
Aiden Bennerc08566e2018-10-03 17:52:42 +00004951def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004952 """Generates a diff command."""
4953 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00004954 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
4955
Aiden Benner6c18a1a2018-11-23 20:18:23 +00004956 if allow_prefix:
4957 # explicitly setting --src-prefix and --dst-prefix is necessary in the
4958 # case that diff.noprefix is set in the user's git config.
4959 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
4960 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00004961 diff_cmd += ['--no-prefix']
4962
4963 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004964
4965 if args:
4966 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004967 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004968 diff_cmd.append(arg)
4969 else:
4970 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004971
4972 return diff_cmd
4973
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004974
Jamie Madill5e96ad12020-01-13 16:08:35 +00004975def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
4976 """Runs clang-format-diff and sets a return value if necessary."""
4977
4978 if not clang_diff_files:
4979 return 0
4980
4981 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4982 # formatted. This is used to block during the presubmit.
4983 return_value = 0
4984
4985 # Locate the clang-format binary in the checkout
4986 try:
4987 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4988 except clang_format.NotFoundError as e:
4989 DieWithError(e)
4990
4991 if opts.full or settings.GetFormatFullByDefault():
4992 cmd = [clang_format_tool]
4993 if not opts.dry_run and not opts.diff:
4994 cmd.append('-i')
4995 if opts.dry_run:
4996 for diff_file in clang_diff_files:
4997 with open(diff_file, 'r') as myfile:
4998 code = myfile.read().replace('\r\n', '\n')
4999 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5000 stdout = stdout.replace('\r\n', '\n')
5001 if opts.diff:
5002 sys.stdout.write(stdout)
5003 if code != stdout:
5004 return_value = 2
5005 else:
5006 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5007 if opts.diff:
5008 sys.stdout.write(stdout)
5009 else:
5010 env = os.environ.copy()
5011 env['PATH'] = str(os.path.dirname(clang_format_tool))
5012 try:
5013 script = clang_format.FindClangFormatScriptInChromiumTree(
5014 'clang-format-diff.py')
5015 except clang_format.NotFoundError as e:
5016 DieWithError(e)
5017
5018 cmd = [sys.executable, script, '-p0']
5019 if not opts.dry_run and not opts.diff:
5020 cmd.append('-i')
5021
5022 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00005023 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00005024
5025 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5026 if opts.diff:
5027 sys.stdout.write(stdout)
5028 if opts.dry_run and len(stdout) > 0:
5029 return_value = 2
5030
5031 return return_value
5032
5033
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005034def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005035 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005036 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005037
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005038
enne@chromium.org555cfe42014-01-29 18:21:39 +00005039@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005040@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005041def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005042 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005043 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005044 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005045 parser.add_option('--full', action='store_true',
5046 help='Reformat the full content of all touched files')
5047 parser.add_option('--dry-run', action='store_true',
5048 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005049 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005050 '--no-clang-format',
5051 dest='clang_format',
5052 action='store_false',
5053 default=True,
5054 help='Disables formatting of various file types using clang-format.')
5055 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005056 '--python',
5057 action='store_true',
5058 default=None,
5059 help='Enables python formatting on all python files.')
5060 parser.add_option(
5061 '--no-python',
5062 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005063 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005064 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005065 'If neither --python or --no-python are set, python files that have a '
5066 '.style.yapf file in an ancestor directory will be formatted. '
5067 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005068 parser.add_option(
5069 '--js',
5070 action='store_true',
5071 help='Format javascript code with clang-format. '
5072 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005073 parser.add_option('--diff', action='store_true',
5074 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005075 parser.add_option('--presubmit', action='store_true',
5076 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005077 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005078
Garrett Beaty91a6f332020-01-06 16:57:24 +00005079 if opts.python is not None and opts.no_python:
5080 raise parser.error('Cannot set both --python and --no-python')
5081 if opts.no_python:
5082 opts.python = False
5083
Daniel Chengc55eecf2016-12-30 03:11:02 -08005084 # Normalize any remaining args against the current path, so paths relative to
5085 # the current directory are still resolved as expected.
5086 args = [os.path.join(os.getcwd(), arg) for arg in args]
5087
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005088 # git diff generates paths against the root of the repository. Change
5089 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005090 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005091 if rel_base_path:
5092 os.chdir(rel_base_path)
5093
digit@chromium.org29e47272013-05-17 17:01:46 +00005094 # Grab the merge-base commit, i.e. the upstream commit of the current
5095 # branch when it was created or the last time it was rebased. This is
5096 # to cover the case where the user may have called "git fetch origin",
5097 # moving the origin branch to a newer commit, but hasn't rebased yet.
5098 upstream_commit = None
5099 cl = Changelist()
5100 upstream_branch = cl.GetUpstreamBranch()
5101 if upstream_branch:
5102 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5103 upstream_commit = upstream_commit.strip()
5104
5105 if not upstream_commit:
5106 DieWithError('Could not find base commit for this branch. '
5107 'Are you in detached state?')
5108
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005109 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5110 diff_output = RunGit(changed_files_cmd)
5111 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005112 # Filter out files deleted by this CL
5113 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005114
Andreas Haas417d89c2020-02-06 10:24:27 +00005115 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005116 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005117
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005118 clang_diff_files = []
5119 if opts.clang_format:
5120 clang_diff_files = [
5121 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5122 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005123 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5124 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005125 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005126
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005127 top_dir = os.path.normpath(
5128 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5129
Jamie Madill5e96ad12020-01-13 16:08:35 +00005130 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5131 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005132
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005133 # Similar code to above, but using yapf on .py files rather than clang-format
5134 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005135 py_explicitly_disabled = opts.python is not None and not opts.python
5136 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005137 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5138 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5139 if sys.platform.startswith('win'):
5140 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005141
Aiden Bennerc08566e2018-10-03 17:52:42 +00005142 # Used for caching.
5143 yapf_configs = {}
5144 for f in python_diff_files:
5145 # Find the yapf style config for the current file, defaults to depot
5146 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005147 _FindYapfConfigFile(f, yapf_configs, top_dir)
5148
5149 # Turn on python formatting by default if a yapf config is specified.
5150 # This breaks in the case of this repo though since the specified
5151 # style file is also the global default.
5152 if opts.python is None:
5153 filtered_py_files = []
5154 for f in python_diff_files:
5155 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5156 filtered_py_files.append(f)
5157 else:
5158 filtered_py_files = python_diff_files
5159
5160 # Note: yapf still seems to fix indentation of the entire file
5161 # even if line ranges are specified.
5162 # See https://github.com/google/yapf/issues/499
5163 if not opts.full and filtered_py_files:
5164 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5165
Brian Sheedyb4307d52019-12-02 19:18:17 +00005166 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
5167 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
5168 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00005169
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005170 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005171 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
5172 # Default to pep8 if not .style.yapf is found.
5173 if not yapf_style:
5174 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00005175
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005176 cmd = [yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00005177
5178 has_formattable_lines = False
5179 if not opts.full:
5180 # Only run yapf over changed line ranges.
5181 for diff_start, diff_len in py_line_diffs[f]:
5182 diff_end = diff_start + diff_len - 1
5183 # Yapf errors out if diff_end < diff_start but this
5184 # is a valid line range diff for a removal.
5185 if diff_end >= diff_start:
5186 has_formattable_lines = True
5187 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5188 # If all line diffs were removals we have nothing to format.
5189 if not has_formattable_lines:
5190 continue
5191
5192 if opts.diff or opts.dry_run:
5193 cmd += ['--diff']
5194 # Will return non-zero exit code if non-empty diff.
5195 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5196 if opts.diff:
5197 sys.stdout.write(stdout)
5198 elif len(stdout) > 0:
5199 return_value = 2
5200 else:
5201 cmd += ['-i']
5202 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005203
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005204 # Dart's formatter does not have the nice property of only operating on
5205 # modified chunks, so hard code full.
5206 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005207 try:
5208 command = [dart_format.FindDartFmtToolInChromiumTree()]
5209 if not opts.dry_run and not opts.diff:
5210 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005211 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005212
ppi@chromium.org6593d932016-03-03 15:41:15 +00005213 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005214 if opts.dry_run and stdout:
5215 return_value = 2
Jamie Madill5e96ad12020-01-13 16:08:35 +00005216 except dart_format.NotFoundError:
vapiera7fbd5a2016-06-16 09:17:49 -07005217 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5218 'found in this checkout. Files in other languages are still '
5219 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005220
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005221 # Format GN build files. Always run on full build files for canonical form.
5222 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005223 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005224 if opts.dry_run or opts.diff:
5225 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005226 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005227 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5228 shell=sys.platform == 'win32',
5229 cwd=top_dir)
5230 if opts.dry_run and gn_ret == 2:
5231 return_value = 2 # Not formatted.
5232 elif opts.diff and gn_ret == 2:
5233 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005234 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005235 elif gn_ret != 0:
5236 # For non-dry run cases (and non-2 return values for dry-run), a
5237 # nonzero error code indicates a failure, probably because the file
5238 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005239 DieWithError('gn format failed on ' + gn_diff_file +
5240 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005241
Ilya Shermane081cbe2017-08-15 17:51:04 -07005242 # Skip the metrics formatting from the global presubmit hook. These files have
5243 # a separate presubmit hook that issues an error if the files need formatting,
5244 # whereas the top-level presubmit script merely issues a warning. Formatting
5245 # these files is somewhat slow, so it's important not to duplicate the work.
5246 if not opts.presubmit:
5247 for xml_dir in GetDirtyMetricsDirs(diff_files):
5248 tool_dir = os.path.join(top_dir, xml_dir)
5249 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5250 if opts.dry_run or opts.diff:
5251 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005252 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005253 if opts.diff:
5254 sys.stdout.write(stdout)
5255 if opts.dry_run and stdout:
5256 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005257
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005258 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005259
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005260
Steven Holte2e664bf2017-04-21 13:10:47 -07005261def GetDirtyMetricsDirs(diff_files):
5262 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5263 metrics_xml_dirs = [
5264 os.path.join('tools', 'metrics', 'actions'),
5265 os.path.join('tools', 'metrics', 'histograms'),
5266 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005267 os.path.join('tools', 'metrics', 'ukm'),
5268 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005269 for xml_dir in metrics_xml_dirs:
5270 if any(file.startswith(xml_dir) for file in xml_diff_files):
5271 yield xml_dir
5272
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005273
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005274@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005275@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005276def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005277 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005278 _, args = parser.parse_args(args)
5279
5280 if len(args) != 1:
5281 parser.print_help()
5282 return 1
5283
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005284 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005285 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005286 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005287
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005288 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005289
Edward Lemur52969c92020-02-06 18:15:28 +00005290 issueprefix = Changelist.IssueConfigKey()
5291 output = RunGit(['config', '--local', '--get-regexp',
5292 r'branch\..*\.%s' % issueprefix],
5293 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005294
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005295 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00005296 for key, issue in [x.split() for x in output.splitlines()]:
5297 if issue == target_issue:
5298 branches.append(re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key))
5299
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005300 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005301 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005302 return 1
5303 if len(branches) == 1:
5304 RunGit(['checkout', branches[0]])
5305 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005306 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005307 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005308 print('%d: %s' % (i, branches[i]))
Edward Lemur1a83da12020-03-04 21:18:36 +00005309 which = ask_for_data('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005310 try:
5311 RunGit(['checkout', branches[int(which)]])
5312 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005313 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005314 return 1
5315
5316 return 0
5317
5318
maruel@chromium.org29404b52014-09-08 22:58:00 +00005319def CMDlol(parser, args):
5320 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005321 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005322 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5323 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5324 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005325 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005326 return 0
5327
5328
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005329class OptionParser(optparse.OptionParser):
5330 """Creates the option parse and add --verbose support."""
5331 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005332 optparse.OptionParser.__init__(
5333 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005334 self.add_option(
5335 '-v', '--verbose', action='count', default=0,
5336 help='Use 2 times for more debugging info')
5337
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005338 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005339 try:
5340 return self._parse_args(args)
5341 finally:
5342 # Regardless of success or failure of args parsing, we want to report
5343 # metrics, but only after logging has been initialized (if parsing
5344 # succeeded).
5345 global settings
5346 settings = Settings()
5347
5348 if not metrics.DISABLE_METRICS_COLLECTION:
5349 # GetViewVCUrl ultimately calls logging method.
5350 project_url = settings.GetViewVCUrl().strip('/+')
5351 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5352 metrics.collector.add('project_urls', [project_url])
5353
5354 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005355 # Create an optparse.Values object that will store only the actual passed
5356 # options, without the defaults.
5357 actual_options = optparse.Values()
5358 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5359 # Create an optparse.Values object with the default options.
5360 options = optparse.Values(self.get_default_values().__dict__)
5361 # Update it with the options passed by the user.
5362 options._update_careful(actual_options.__dict__)
5363 # Store the options passed by the user in an _actual_options attribute.
5364 # We store only the keys, and not the values, since the values can contain
5365 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005366 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005367
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005368 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005369 logging.basicConfig(
5370 level=levels[min(options.verbose, len(levels) - 1)],
5371 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5372 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005373
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005374 return options, args
5375
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005377def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005378 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005379 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005380 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005381 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005382
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005383 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005384 dispatcher = subcommand.CommandDispatcher(__name__)
5385 try:
5386 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005387 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005388 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005389 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005390 if e.code != 500:
5391 raise
5392 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005393 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005394 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005395 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005396
5397
5398if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005399 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5400 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005401 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005402 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005403 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005404 sys.exit(main(sys.argv[1:]))