blob: 9f8e61c0c36a35f067f8ed4dbfd8eecb4e3c91ab [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
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010016import datetime
Brian Sheedy59b06a82019-10-14 17:03:29 +000017import glob
Edward Lemur202c5592019-10-21 22:44:52 +000018import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010019import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000022import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import optparse
24import os
25import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010026import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000027import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070029import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000031import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000036from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000037import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000038import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000039import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000040import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000041import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000042import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000043import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000044import git_footers
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')
71
72# When collecting traces, Git hashes will be reduced to 6 characters to reduce
73# the size after compression.
74GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
75# Used to redact the cookies from the gitcookies file.
76GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
77
Edward Lemurd4d1ba42019-09-20 21:46:37 +000078MAX_ATTEMPTS = 3
79
Edward Lemur1b52d872019-05-09 21:12:12 +000080# The maximum number of traces we will keep. Multiplied by 3 since we store
81# 3 files per trace.
82MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000083# Message to be displayed to the user to inform where to find the traces for a
84# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000085TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000086'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000087'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000088' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000089'Copies of your gitcookies file and git config have been recorded at:\n'
90' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000091# Format of the message to be stored as part of the traces to give developers a
92# better context when they go through traces.
93TRACES_README_FORMAT = (
94'Date: %(now)s\n'
95'\n'
96'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
97'Title: %(title)s\n'
98'\n'
99'%(description)s\n'
100'\n'
101'Execution time: %(execution_time)s\n'
102'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000103
tandrii9d2c7a32016-06-22 03:42:45 -0700104COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800105POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +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
Christopher Lamf732cd52017-01-24 12:40:11 +1100132def DieWithError(message, change_desc=None):
133 if change_desc:
134 SaveDescriptionBackup(change_desc)
135
vapiera7fbd5a2016-06-16 09:17:49 -0700136 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000137 sys.exit(1)
138
139
Christopher Lamf732cd52017-01-24 12:40:11 +1100140def SaveDescriptionBackup(change_desc):
141 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000142 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100143 backup_file = open(backup_path, 'w')
144 backup_file.write(change_desc.description)
145 backup_file.close()
146
147
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000148def GetNoGitPagerEnv():
149 env = os.environ.copy()
150 # 'cat' is a magical git string that disables pagers on all platforms.
151 env['GIT_PAGER'] = 'cat'
152 return env
153
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000154
bsep@chromium.org627d9002016-04-29 00:00:52 +0000155def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000156 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000157 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
158 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000159 except subprocess2.CalledProcessError as e:
160 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000161 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000163 'Command "%s" failed.\n%s' % (
164 ' '.join(args), error_message or e.stdout or ''))
Edward Lemur79d4f992019-11-11 23:49:02 +0000165 return e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000166
167
168def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000169 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000170 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171
172
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000173def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000174 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700175 if suppress_stderr:
176 stderr = subprocess2.VOID
177 else:
178 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000179 try:
tandrii5d48c322016-08-18 16:19:37 -0700180 (out, _), code = subprocess2.communicate(['git'] + args,
181 env=GetNoGitPagerEnv(),
182 stdout=subprocess2.PIPE,
183 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000184 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700185 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900186 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000187 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000188
189
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000190def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000191 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000192 return RunGitWithCode(args, suppress_stderr=True)[1]
193
194
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000195def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000196 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000197 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000198 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000199 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000200
201
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000202def BranchExists(branch):
203 """Return True if specified branch exists."""
204 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
205 suppress_stderr=True)
206 return not code
207
208
tandrii2a16b952016-10-19 07:09:44 -0700209def time_sleep(seconds):
210 # Use this so that it can be mocked in tests without interfering with python
211 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700212 return time.sleep(seconds)
213
214
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000215def time_time():
216 # Use this so that it can be mocked in tests without interfering with python
217 # system machinery.
218 return time.time()
219
220
Edward Lemur1b52d872019-05-09 21:12:12 +0000221def datetime_now():
222 # Use this so that it can be mocked in tests without interfering with python
223 # system machinery.
224 return datetime.datetime.now()
225
226
maruel@chromium.org90541732011-04-01 17:54:18 +0000227def ask_for_data(prompt):
228 try:
229 return raw_input(prompt)
230 except KeyboardInterrupt:
231 # Hide the exception.
232 sys.exit(1)
233
234
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100235def confirm_or_exit(prefix='', action='confirm'):
236 """Asks user to press enter to continue or press Ctrl+C to abort."""
237 if not prefix or prefix.endswith('\n'):
238 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100239 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100240 mid = ' Press'
241 elif prefix.endswith(' '):
242 mid = 'press'
243 else:
244 mid = ' press'
245 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
246
247
248def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000249 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100250 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
251 while True:
252 if 'yes'.startswith(result):
253 return True
254 if 'no'.startswith(result):
255 return False
256 result = ask_for_data('Please, type yes or no: ').lower()
257
258
tandrii5d48c322016-08-18 16:19:37 -0700259def _git_branch_config_key(branch, key):
260 """Helper method to return Git config key for a branch."""
261 assert branch, 'branch name is required to set git config for it'
262 return 'branch.%s.%s' % (branch, key)
263
264
265def _git_get_branch_config_value(key, default=None, value_type=str,
266 branch=False):
267 """Returns git config value of given or current branch if any.
268
269 Returns default in all other cases.
270 """
271 assert value_type in (int, str, bool)
272 if branch is False: # Distinguishing default arg value from None.
273 branch = GetCurrentBranch()
274
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000275 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700276 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000277
tandrii5d48c322016-08-18 16:19:37 -0700278 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700279 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700280 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000281 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700282 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700283 args.append(_git_branch_config_key(branch, key))
284 code, out = RunGitWithCode(args)
285 if code == 0:
286 value = out.strip()
287 if value_type == int:
288 return int(value)
289 if value_type == bool:
290 return bool(value.lower() == 'true')
291 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000292 return default
293
294
tandrii5d48c322016-08-18 16:19:37 -0700295def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000296 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700297
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000298 If value is None, the key will be unset, otherwise it will be set.
299 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700300 """
301 if not branch:
302 branch = GetCurrentBranch()
303 assert branch, 'a branch name OR currently checked out branch is required'
304 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700305 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700306 if value is None:
307 args.append('--unset')
308 elif isinstance(value, bool):
309 args.append('--bool')
310 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700311 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000312 # `git config` also has --int, but apparently git config suffers from
313 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700314 value = str(value)
315 args.append(_git_branch_config_key(branch, key))
316 if value is not None:
317 args.append(value)
318 RunGit(args, **kwargs)
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000322 prop_list = getattr(options, 'properties', [])
323 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000324 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000325 try:
326 properties[key] = json.loads(val)
327 except ValueError:
328 pass # If a value couldn't be evaluated, treat it as a string.
329 return properties
330
331
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000332# TODO(crbug.com/976104): Remove this function once git-cl try-results has
333# migrated to use buildbucket v2
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000334def _buildbucket_retry(operation_name, http, *args, **kwargs):
335 """Retries requests to buildbucket service and returns parsed json content."""
336 try_count = 0
337 while True:
338 response, content = http.request(*args, **kwargs)
339 try:
340 content_json = json.loads(content)
341 except ValueError:
342 content_json = None
343
344 # Buildbucket could return an error even if status==200.
345 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000346 error = content_json.get('error')
347 if error.get('code') == 403:
348 raise BuildbucketResponseException(
349 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000351 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 raise BuildbucketResponseException(msg)
353
354 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700355 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000357 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700358 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000359 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 content)
361 return content_json
362 if response.status < 500 or try_count >= 2:
363 raise httplib2.HttpLib2Error(content)
364
365 # status >= 500 means transient failures.
366 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000367 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368 try_count += 1
369 assert False, 'unreachable'
370
371
Edward Lemur4c707a22019-09-24 21:13:43 +0000372def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000373 """Calls a buildbucket v2 method and returns the parsed json response."""
374 headers = {
375 'Accept': 'application/json',
376 'Content-Type': 'application/json',
377 }
378 request = json.dumps(request)
379 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
380
381 logging.info('POST %s with %s' % (url, request))
382
383 attempts = 1
384 time_to_sleep = 1
385 while True:
386 response, content = http.request(url, 'POST', body=request, headers=headers)
387 if response.status == 200:
388 return json.loads(content[4:])
389 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
390 msg = '%s error when calling POST %s with %s: %s' % (
391 response.status, url, request, content)
392 raise BuildbucketResponseException(msg)
393 logging.debug(
394 '%s error when calling POST %s with %s. '
395 'Sleeping for %d seconds and retrying...' % (
396 response.status, url, request, time_to_sleep))
397 time.sleep(time_to_sleep)
398 time_to_sleep *= 2
399 attempts += 1
400
401 assert False, 'unreachable'
402
403
qyearsley1fdfcb62016-10-24 13:22:03 -0700404def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700405 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000406 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 """
qyearsleydd49f942016-10-28 11:57:22 -0700408 # If no bots are listed, we try to get a set of builders and tests based
409 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 if not options.bot:
411 change = changelist.GetChange(
412 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700413 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700414 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700415 change=change,
416 changed_files=change.LocalPaths(),
417 repository_root=settings.GetRoot(),
418 default_presubmit=None,
419 project=None,
420 verbose=options.verbose,
421 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700422 if masters is None:
423 return None
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000424 return {m: b for m, b in masters.items()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700425
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 if options.bucket:
427 return {options.bucket: {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000428 option_parser.error(
Edward Lemur5ef16a32019-11-11 21:13:25 +0000429 'Please specify the bucket, e.g. "-B chromium/try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700430
431
Edward Lemur6215c792019-10-03 21:59:05 +0000432def _parse_bucket(raw_bucket):
433 legacy = True
434 project = bucket = None
435 if '/' in raw_bucket:
436 legacy = False
437 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000438 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000439 elif raw_bucket.startswith('luci.'):
440 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000441 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000442 elif '.' in raw_bucket:
443 project = raw_bucket.split('.')[0]
444 bucket = raw_bucket
445 # Legacy buckets.
446 if legacy:
447 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
448 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000449
450
Edward Lemur5b929a42019-10-21 17:57:39 +0000451def _trigger_try_jobs(changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000452 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700453
454 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000455 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700456 buckets: A nested dict mapping bucket names to builders to tests.
457 options: Command-line options.
458 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000459 print('Scheduling jobs on:')
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000460 for bucket, builders_and_tests in sorted(buckets.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000461 print('Bucket:', bucket)
462 print('\n'.join(
463 ' %s: %s' % (builder, tests)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000464 for builder, tests in sorted(builders_and_tests.items())))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000465 print('To see results here, run: git cl try-results')
466 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700467
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000468 requests = _make_try_job_schedule_requests(
469 changelist, buckets, options, patchset)
470 if not requests:
471 return
472
Edward Lemur5b929a42019-10-21 17:57:39 +0000473 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000474 http.force_exception_to_status_code = True
475
476 batch_request = {'requests': requests}
477 batch_response = _call_buildbucket(
478 http, options.buildbucket_host, 'Batch', batch_request)
479
480 errors = [
481 ' ' + response['error']['message']
482 for response in batch_response.get('responses', [])
483 if 'error' in response
484 ]
485 if errors:
486 raise BuildbucketResponseException(
487 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
488
489
490def _make_try_job_schedule_requests(changelist, buckets, options, patchset):
Edward Lemurf0faf482019-09-25 20:40:17 +0000491 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000492 shared_properties = {'category': getattr(options, 'category', 'git_cl_try')}
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000493 if getattr(options, 'clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000494 shared_properties['clobber'] = True
495 shared_properties.update(_get_properties_from_options(options) or {})
496
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000497 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
498 if options.retry_failed:
499 shared_tags.append({'key': 'retry_failed',
500 'value': '1'})
501
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000502 requests = []
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000503 for raw_bucket, builders_and_tests in sorted(buckets.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000504 project, bucket = _parse_bucket(raw_bucket)
505 if not project or not bucket:
506 print('WARNING Could not parse bucket "%s". Skipping.' % raw_bucket)
507 continue
508
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000509 for builder, tests in sorted(builders_and_tests.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000510 properties = shared_properties.copy()
511 if 'presubmit' in builder.lower():
512 properties['dry_run'] = 'true'
513 if tests:
514 properties['testfilter'] = tests
515
516 requests.append({
517 'scheduleBuild': {
518 'requestId': str(uuid.uuid4()),
519 'builder': {
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000520 'project': getattr(options, 'project', None) or project,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000521 'bucket': bucket,
522 'builder': builder,
523 },
524 'gerritChanges': gerrit_changes,
525 'properties': properties,
526 'tags': [
527 {'key': 'builder', 'value': builder},
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000528 ] + shared_tags,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000529 }
530 })
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000531 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000532
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000533
Edward Lemur5b929a42019-10-21 17:57:39 +0000534def fetch_try_jobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000535 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000537 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000539 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000540 request = {
541 'predicate': {
542 'gerritChanges': [changelist.GetGerritChange(patchset)],
543 },
544 'fields': ','.join('builds.*.' + field for field in fields),
545 }
tandrii221ab252016-10-06 08:12:04 -0700546
Edward Lemur5b929a42019-10-21 17:57:39 +0000547 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 if authenticator.has_cached_credentials():
549 http = authenticator.authorize(httplib2.Http())
550 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700551 print('Warning: Some results might be missing because %s' %
552 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000553 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000554 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000555 http.force_exception_to_status_code = True
556
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000557 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
558 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559
Edward Lemur5b929a42019-10-21 17:57:39 +0000560def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000561 """Fetches builds from the latest patchset that has builds (within
562 the last few patchsets).
563
564 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000565 changelist (Changelist): The CL to fetch builds for
566 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000567 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
568 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000569 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000570 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
571 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000572 """
573 assert buildbucket_host
574 assert changelist.GetIssue(), 'CL must be uploaded first'
575 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000576 if latest_patchset is None:
577 assert changelist.GetMostRecentPatchset()
578 ps = changelist.GetMostRecentPatchset()
579 else:
580 assert latest_patchset > 0, latest_patchset
581 ps = latest_patchset
582
Quinten Yearsley983111f2019-09-26 17:18:48 +0000583 min_ps = max(1, ps - 5)
584 while ps >= min_ps:
Edward Lemur5b929a42019-10-21 17:57:39 +0000585 builds = fetch_try_jobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000586 if len(builds):
587 return builds, ps
588 ps -= 1
589 return [], 0
590
591
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000592def _filter_failed_for_retry(all_builds):
593 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000594
595 Args:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000596 all_builds (list): Builds, in the format returned by fetch_try_jobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000597 i.e. a list of buildbucket.v2.Builds which includes status and builder
598 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000599
600 Returns:
601 A dict of bucket to builder to tests (empty list). This is the same format
602 accepted by _trigger_try_jobs and returned by _get_bucket_map.
603 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000604
605 def _builder_of(build):
606 builder = build['builder']
607 return (builder['project'], builder['bucket'], builder['builder'])
608
609 res = collections.defaultdict(dict)
610 ordered = sorted(all_builds, key=lambda b: (_builder_of(b), b['createTime']))
611 for (proj, buck, bldr), builds in itertools.groupby(ordered, key=_builder_of):
612 # If builder had several builds, retry only if the last one failed.
613 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
614 # build, but in case of retrying failed jobs retrying a flaky one makes
615 # sense.
616 builds = list(builds)
617 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
618 continue
619 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
620 for t in builds[-1]['tags']):
621 # Don't retry experimental build previously triggered by CQ.
622 continue
623 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
624 # Don't retry if any are running.
625 continue
626 res[proj + '/' + buck][bldr] = []
627 return res
Quinten Yearsley983111f2019-09-26 17:18:48 +0000628
629
qyearsleyeab3c042016-08-24 09:18:28 -0700630def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631 """Prints nicely result of fetch_try_jobs."""
632 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000633 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000634 return
635
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000636 longest_builder = max(len(b['builder']['builder']) for b in builds)
637 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000639 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
640 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000641
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000642 builds_by_status = {}
643 for b in builds:
644 builds_by_status.setdefault(b['status'], []).append({
645 'id': b['id'],
646 'name': name_fmt.format(
647 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
648 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000650 sort_key = lambda b: (b['name'], b['id'])
651
652 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000653 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000654 if not builds:
655 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000656
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000657 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000658 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000659 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000660 else:
661 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
662
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000663 print(colorize(title))
664 for b in sorted(builds, key=sort_key):
665 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666
667 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000668 print_builds(
669 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
670 print_builds(
671 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
672 color=Fore.MAGENTA)
673 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
674 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
675 color=Fore.MAGENTA)
676 print_builds('Started:', builds_by_status.pop('STARTED', []))
677 print_builds(
678 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000679 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000680 print_builds(
681 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000682 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000683
684
Aiden Bennerc08566e2018-10-03 17:52:42 +0000685def _ComputeDiffLineRanges(files, upstream_commit):
686 """Gets the changed line ranges for each file since upstream_commit.
687
688 Parses a git diff on provided files and returns a dict that maps a file name
689 to an ordered list of range tuples in the form (start_line, count).
690 Ranges are in the same format as a git diff.
691 """
692 # If files is empty then diff_output will be a full diff.
693 if len(files) == 0:
694 return {}
695
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000696 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000697 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000698 diff_output = RunGit(diff_cmd)
699
700 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
701 # 2 capture groups
702 # 0 == fname of diff file
703 # 1 == 'diff_start,diff_count' or 'diff_start'
704 # will match each of
705 # diff --git a/foo.foo b/foo.py
706 # @@ -12,2 +14,3 @@
707 # @@ -12,2 +17 @@
708 # running re.findall on the above string with pattern will give
709 # [('foo.py', ''), ('', '14,3'), ('', '17')]
710
711 curr_file = None
712 line_diffs = {}
713 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
714 if match[0] != '':
715 # Will match the second filename in diff --git a/a.py b/b.py.
716 curr_file = match[0]
717 line_diffs[curr_file] = []
718 else:
719 # Matches +14,3
720 if ',' in match[1]:
721 diff_start, diff_count = match[1].split(',')
722 else:
723 # Single line changes are of the form +12 instead of +12,1.
724 diff_start = match[1]
725 diff_count = 1
726
727 diff_start = int(diff_start)
728 diff_count = int(diff_count)
729
730 # If diff_count == 0 this is a removal we can ignore.
731 line_diffs[curr_file].append((diff_start, diff_count))
732
733 return line_diffs
734
735
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000736def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000737 """Checks if a yapf file is in any parent directory of fpath until top_dir.
738
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000739 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000740 is found returns None. Uses yapf_config_cache as a cache for previously found
741 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000742 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000743 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000744 # Return result if we've already computed it.
745 if fpath in yapf_config_cache:
746 return yapf_config_cache[fpath]
747
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000748 parent_dir = os.path.dirname(fpath)
749 if os.path.isfile(fpath):
750 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000751 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000752 # Otherwise fpath is a directory
753 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
754 if os.path.isfile(yapf_file):
755 ret = yapf_file
756 elif fpath == top_dir or parent_dir == fpath:
757 # If we're at the top level directory, or if we're at root
758 # there is no provided style.
759 ret = None
760 else:
761 # Otherwise recurse on the current directory.
762 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000763 yapf_config_cache[fpath] = ret
764 return ret
765
766
Brian Sheedy59b06a82019-10-14 17:03:29 +0000767def _GetYapfIgnoreFilepaths(top_dir):
768 """Returns all filepaths that match the ignored files in the .yapfignore file.
769
770 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
771 but this functionality appears to break when explicitly passing files to
772 yapf for formatting. According to
773 https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
774 the .yapfignore file should be in the directory that yapf is invoked from,
775 which we assume to be the top level directory in this case.
776
777 Args:
778 top_dir: The top level directory for the repository being formatted.
779
780 Returns:
781 A set of all filepaths that should be ignored by yapf.
782 """
783 yapfignore_file = os.path.join(top_dir, '.yapfignore')
784 ignore_filepaths = set()
785 if not os.path.exists(yapfignore_file):
786 return ignore_filepaths
787
788 # glob works relative to the current working directory, so we need to ensure
789 # that we're at the top level directory.
790 old_cwd = os.getcwd()
791 try:
792 os.chdir(top_dir)
793 with open(yapfignore_file) as f:
794 for line in f.readlines():
795 stripped_line = line.strip()
796 # Comments and blank lines should be ignored.
797 if stripped_line.startswith('#') or stripped_line == '':
798 continue
799 ignore_filepaths |= set(glob.glob(stripped_line))
800 return ignore_filepaths
801 finally:
802 os.chdir(old_cwd)
803
804
Aaron Gable13101a62018-02-09 13:20:41 -0800805def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000806 """Prints statistics about the change to the user."""
807 # --no-ext-diff is broken in some versions of Git, so try to work around
808 # this by overriding the environment (but there is still a problem if the
809 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000811 if 'GIT_EXTERNAL_DIFF' in env:
812 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000813
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000814 try:
815 stdout = sys.stdout.fileno()
816 except AttributeError:
817 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000818 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800819 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000820 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000821
822
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000823class BuildbucketResponseException(Exception):
824 pass
825
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827class Settings(object):
828 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000830 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 self.tree_status_url = None
832 self.viewvc_url = None
833 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000834 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000835 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000836 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000837 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000838 self.format_full_by_default = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839
840 def LazyUpdateIfNeeded(self):
841 """Updates the settings from a codereview.settings file, if available."""
842 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000843 # The only value that actually changes the behavior is
844 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000845 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000846 error_ok=True
847 ).strip().lower()
848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000850 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 LoadCodereviewSettingsFromFile(cr_settings_file)
852 self.updated = True
853
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000854 @staticmethod
855 def GetRelativeRoot():
856 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000859 if self.root is None:
860 self.root = os.path.abspath(self.GetRelativeRoot())
861 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 def GetTreeStatusUrl(self, error_ok=False):
864 if not self.tree_status_url:
865 error_message = ('You must configure your tree status URL by running '
866 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000867 self.tree_status_url = self._GetConfig(
868 'rietveld.tree-status-url', error_ok=error_ok,
869 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 return self.tree_status_url
871
872 def GetViewVCUrl(self):
873 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000874 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 return self.viewvc_url
876
rmistry@google.com90752582014-01-14 21:04:50 +0000877 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000878 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000879
rmistry@google.com5626a922015-02-26 14:03:30 +0000880 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000881 run_post_upload_hook = self._GetConfig(
882 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000883 return run_post_upload_hook == "True"
884
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000885 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000886 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000887
ukai@chromium.orge8077812012-02-03 03:41:46 +0000888 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000889 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000890 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700891 self.is_gerrit = (
892 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000893 return self.is_gerrit
894
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000895 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000896 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000897 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700898 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
899 if self.squash_gerrit_uploads is None:
900 # Default is squash now (http://crbug.com/611892#c23).
901 self.squash_gerrit_uploads = not (
902 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
903 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000904 return self.squash_gerrit_uploads
905
tandriia60502f2016-06-20 02:01:53 -0700906 def GetSquashGerritUploadsOverride(self):
907 """Return True or False if codereview.settings should be overridden.
908
909 Returns None if no override has been defined.
910 """
911 # See also http://crbug.com/611892#c23
912 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
913 error_ok=True).strip()
914 if result == 'true':
915 return True
916 if result == 'false':
917 return False
918 return None
919
tandrii@chromium.org28253532016-04-14 13:46:56 +0000920 def GetGerritSkipEnsureAuthenticated(self):
921 """Return True if EnsureAuthenticated should not be done for Gerrit
922 uploads."""
923 if self.gerrit_skip_ensure_authenticated is None:
924 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000925 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000926 error_ok=True).strip() == 'true')
927 return self.gerrit_skip_ensure_authenticated
928
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000929 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000930 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000931 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000932 # Git requires single quotes for paths with spaces. We need to replace
933 # them with double quotes for Windows to treat such paths as a single
934 # path.
935 self.git_editor = self._GetConfig(
936 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000937 return self.git_editor or None
938
thestig@chromium.org44202a22014-03-11 19:22:18 +0000939 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000940 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000941 DEFAULT_LINT_REGEX)
942
943 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000944 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000945 DEFAULT_LINT_IGNORE_REGEX)
946
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000947 def GetFormatFullByDefault(self):
948 if self.format_full_by_default is None:
949 result = (
950 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
951 error_ok=True).strip())
952 self.format_full_by_default = (result == 'true')
953 return self.format_full_by_default
954
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 def _GetConfig(self, param, **kwargs):
956 self.LazyUpdateIfNeeded()
957 return RunGit(['config', param], **kwargs).strip()
958
959
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960def ShortBranchName(branch):
961 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000962 return branch.replace('refs/heads/', '', 1)
963
964
965def GetCurrentBranchRef():
966 """Returns branch ref (e.g., refs/heads/master) or None."""
967 return RunGit(['symbolic-ref', 'HEAD'],
968 stderr=subprocess2.VOID, error_ok=True).strip() or None
969
970
971def GetCurrentBranch():
972 """Returns current branch or None.
973
974 For refs/heads/* branches, returns just last part. For others, full ref.
975 """
976 branchref = GetCurrentBranchRef()
977 if branchref:
978 return ShortBranchName(branchref)
979 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
981
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000982class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000983 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000984 NONE = 'none'
985 DRY_RUN = 'dry_run'
986 COMMIT = 'commit'
987
988 ALL_STATES = [NONE, DRY_RUN, COMMIT]
989
990
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000991class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000992 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000993 self.issue = issue
994 self.patchset = patchset
995 self.hostname = hostname
996
997 @property
998 def valid(self):
999 return self.issue is not None
1000
1001
Edward Lemurf38bc172019-09-03 21:02:13 +00001002def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001003 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1004 fail_result = _ParsedIssueNumberArgument()
1005
Edward Lemur678a6842019-10-03 22:25:05 +00001006 if isinstance(arg, int):
1007 return _ParsedIssueNumberArgument(issue=arg)
1008 if not isinstance(arg, basestring):
1009 return fail_result
1010
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001011 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00001012 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001013 if not arg.startswith('http'):
1014 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001015
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001016 url = gclient_utils.UpgradeToHttps(arg)
1017 try:
Edward Lemur79d4f992019-11-11 23:49:02 +00001018 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001019 except ValueError:
1020 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001021
Edward Lemur678a6842019-10-03 22:25:05 +00001022 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
1023 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
1024 # Short urls like https://domain/<issue_number> can be used, but don't allow
1025 # specifying the patchset (you'd 404), but we allow that here.
1026 if parsed_url.path == '/':
1027 part = parsed_url.fragment
1028 else:
1029 part = parsed_url.path
1030
1031 match = re.match(
1032 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
1033 if not match:
1034 return fail_result
1035
1036 issue = int(match.group('issue'))
1037 patchset = match.group('patchset')
1038 return _ParsedIssueNumberArgument(
1039 issue=issue,
1040 patchset=int(patchset) if patchset else None,
1041 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001042
1043
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001044def _create_description_from_log(args):
1045 """Pulls out the commit log to use as a base for the CL description."""
1046 log_args = []
1047 if len(args) == 1 and not args[0].endswith('.'):
1048 log_args = [args[0] + '..']
1049 elif len(args) == 1 and args[0].endswith('...'):
1050 log_args = [args[0][:-1]]
1051 elif len(args) == 2:
1052 log_args = [args[0] + '..' + args[1]]
1053 else:
1054 log_args = args[:] # Hope for the best!
1055 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1056
1057
Aaron Gablea45ee112016-11-22 15:14:38 -08001058class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001059 def __init__(self, issue, url):
1060 self.issue = issue
1061 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001062 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001063
1064 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001065 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001066 self.issue, self.url)
1067
1068
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001069_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001070 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001071 # TODO(tandrii): these two aren't known in Gerrit.
1072 'approval', 'disapproval'])
1073
1074
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 """Changelist works with one changelist in local branch.
1077
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001078 Notes:
1079 * Not safe for concurrent multi-{thread,process} use.
1080 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001081 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001082 """
1083
Edward Lemur125d60a2019-09-13 18:25:41 +00001084 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001085 """Create a new ChangeList instance.
1086
Edward Lemurf38bc172019-09-03 21:02:13 +00001087 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001088 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001090 global settings
1091 if not settings:
1092 # Happens when git_cl.py is used as a utility library.
1093 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 self.branchref = branchref
1096 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001097 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 self.branch = ShortBranchName(self.branchref)
1099 else:
1100 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001102 self.lookedup_issue = False
1103 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 self.has_description = False
1105 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001106 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001108 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001109 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001110 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001111 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001112
Edward Lemur125d60a2019-09-13 18:25:41 +00001113 # Lazily cached values.
1114 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1115 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1116 # Map from change number (issue) to its detail cache.
1117 self._detail_cache = {}
1118
1119 if codereview_host is not None:
1120 assert not codereview_host.startswith('https://'), codereview_host
1121 self._gerrit_host = codereview_host
1122 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001123
1124 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001125 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001126
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001127 The return value is a string suitable for passing to git cl with the --cc
1128 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001129 """
1130 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001131 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001132 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001133 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1134 return self.cc
1135
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001136 def GetCCListWithoutDefault(self):
1137 """Return the users cc'd on this CL excluding default ones."""
1138 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001139 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001140 return self.cc
1141
Daniel Cheng7227d212017-11-17 08:12:37 -08001142 def ExtendCC(self, more_cc):
1143 """Extends the list of users to cc on this CL based on the changed files."""
1144 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 def GetBranch(self):
1147 """Returns the short branch name, e.g. 'master'."""
1148 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001150 if not branchref:
1151 return None
1152 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.branch = ShortBranchName(self.branchref)
1154 return self.branch
1155
1156 def GetBranchRef(self):
1157 """Returns the full branch name, e.g. 'refs/heads/master'."""
1158 self.GetBranch() # Poke the lazy loader.
1159 return self.branchref
1160
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001161 def ClearBranch(self):
1162 """Clears cached branch data of this object."""
1163 self.branch = self.branchref = None
1164
tandrii5d48c322016-08-18 16:19:37 -07001165 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1166 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1167 kwargs['branch'] = self.GetBranch()
1168 return _git_get_branch_config_value(key, default, **kwargs)
1169
1170 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1171 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1172 assert self.GetBranch(), (
1173 'this CL must have an associated branch to %sset %s%s' %
1174 ('un' if value is None else '',
1175 key,
1176 '' if value is None else ' to %r' % value))
1177 kwargs['branch'] = self.GetBranch()
1178 return _git_set_branch_config_value(key, value, **kwargs)
1179
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001180 @staticmethod
1181 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001182 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 e.g. 'origin', 'refs/heads/master'
1184 """
1185 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001186 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001189 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001191 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1192 error_ok=True).strip()
1193 if upstream_branch:
1194 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001196 # Else, try to guess the origin remote.
1197 remote_branches = RunGit(['branch', '-r']).split()
1198 if 'origin/master' in remote_branches:
1199 # Fall back on origin/master if it exits.
1200 remote = 'origin'
1201 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001203 DieWithError(
1204 'Unable to determine default branch to diff against.\n'
1205 'Either pass complete "git diff"-style arguments, like\n'
1206 ' git cl upload origin/master\n'
1207 'or verify this branch is set up to track another \n'
1208 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209
1210 return remote, upstream_branch
1211
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001212 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001213 upstream_branch = self.GetUpstreamBranch()
1214 if not BranchExists(upstream_branch):
1215 DieWithError('The upstream for the current branch (%s) does not exist '
1216 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001217 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001218 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 def GetUpstreamBranch(self):
1221 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001222 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001223 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001224 upstream_branch = upstream_branch.replace('refs/heads/',
1225 'refs/remotes/%s/' % remote)
1226 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1227 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 self.upstream_branch = upstream_branch
1229 return self.upstream_branch
1230
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001232 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001233 remote, branch = None, self.GetBranch()
1234 seen_branches = set()
1235 while branch not in seen_branches:
1236 seen_branches.add(branch)
1237 remote, branch = self.FetchUpstreamTuple(branch)
1238 branch = ShortBranchName(branch)
1239 if remote != '.' or branch.startswith('refs/remotes'):
1240 break
1241 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001242 remotes = RunGit(['remote'], error_ok=True).split()
1243 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001244 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001245 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001247 logging.warn('Could not determine which remote this change is '
1248 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001249 else:
1250 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001251 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 branch = 'HEAD'
1253 if branch.startswith('refs/remotes'):
1254 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001255 elif branch.startswith('refs/branch-heads/'):
1256 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001257 else:
1258 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001259 return self._remote
1260
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 def GitSanityChecks(self, upstream_git_obj):
1262 """Checks git repo status and ensures diff is from local commits."""
1263
sbc@chromium.org79706062015-01-14 21:18:12 +00001264 if upstream_git_obj is None:
1265 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001266 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001267 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001268 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001269 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001270 return False
1271
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 # Verify the commit we're diffing against is in our current branch.
1273 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1274 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1275 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001276 print('ERROR: %s is not in the current branch. You may need to rebase '
1277 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001278 return False
1279
1280 # List the commits inside the diff, and verify they are all local.
1281 commits_in_diff = RunGit(
1282 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1283 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1284 remote_branch = remote_branch.strip()
1285 if code != 0:
1286 _, remote_branch = self.GetRemoteBranch()
1287
1288 commits_in_remote = RunGit(
1289 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1290
1291 common_commits = set(commits_in_diff) & set(commits_in_remote)
1292 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001293 print('ERROR: Your diff contains %d commits already in %s.\n'
1294 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1295 'the diff. If you are using a custom git flow, you can override'
1296 ' the reference used for this check with "git config '
1297 'gitcl.remotebranch <git-ref>".' % (
1298 len(common_commits), remote_branch, upstream_git_obj),
1299 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 return False
1301 return True
1302
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001303 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001304 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001305
1306 Returns None if it is not set.
1307 """
tandrii5d48c322016-08-18 16:19:37 -07001308 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 def GetRemoteUrl(self):
1311 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1312
1313 Returns None if there is no remote.
1314 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001315 is_cached, value = self._cached_remote_url
1316 if is_cached:
1317 return value
1318
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001319 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001320 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1321
Edward Lemur298f2cf2019-02-22 21:40:39 +00001322 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001323 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001324 if host:
1325 self._cached_remote_url = (True, url)
1326 return url
1327
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001328 # If it cannot be parsed as an url, assume it is a local directory,
1329 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001330 logging.warning('"%s" doesn\'t appear to point to a git host. '
1331 'Interpreting it as a local directory.', url)
1332 if not os.path.isdir(url):
1333 logging.error(
1334 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
Daniel Bratell4a60db42019-09-16 17:02:52 +00001335 remote, self.GetBranch(), url)
Edward Lemur298f2cf2019-02-22 21:40:39 +00001336 return None
1337
1338 cache_path = url
1339 url = RunGit(['config', 'remote.%s.url' % remote],
1340 error_ok=True,
1341 cwd=url).strip()
1342
Edward Lemur79d4f992019-11-11 23:49:02 +00001343 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001344 if not host:
1345 logging.error(
1346 'Remote "%(remote)s" for branch "%(branch)s" points to '
1347 '"%(cache_path)s", but it is misconfigured.\n'
1348 '"%(cache_path)s" must be a git repo and must have a remote named '
1349 '"%(remote)s" pointing to the git host.', {
1350 'remote': remote,
1351 'cache_path': cache_path,
1352 'branch': self.GetBranch()})
1353 return None
1354
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001355 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001356 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001358 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001359 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001360 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001361 self.issue = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001362 self.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001363 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364 return self.issue
1365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 def GetIssueURL(self):
1367 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001368 issue = self.GetIssue()
1369 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001370 return None
Edward Lemur125d60a2019-09-13 18:25:41 +00001371 return '%s/%s' % (self.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001373 def GetDescription(self, pretty=False, force=False):
1374 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 if self.GetIssue():
Edward Lemur125d60a2019-09-13 18:25:41 +00001376 self.description = self.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 self.has_description = True
1378 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001379 # Set width to 72 columns + 2 space indent.
1380 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001382 lines = self.description.splitlines()
1383 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 return self.description
1385
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001386 def GetDescriptionFooters(self):
1387 """Returns (non_footer_lines, footers) for the commit message.
1388
1389 Returns:
1390 non_footer_lines (list(str)) - Simple list of description lines without
1391 any footer. The lines do not contain newlines, nor does the list contain
1392 the empty line between the message and the footers.
1393 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1394 [("Change-Id", "Ideadbeef...."), ...]
1395 """
1396 raw_description = self.GetDescription()
1397 msg_lines, _, footers = git_footers.split_footers(raw_description)
1398 if footers:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001399 msg_lines = msg_lines[:len(msg_lines) - 1]
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001400 return msg_lines, footers
1401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001403 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001404 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001405 self.patchset = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001406 self.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001407 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 return self.patchset
1409
1410 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001411 """Set this branch's patchset. If patchset=0, clears the patchset."""
1412 assert self.GetBranch()
1413 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001415 else:
1416 self.patchset = int(patchset)
1417 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001418 self.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001420 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001421 """Set this branch's issue. If issue isn't given, clears the issue."""
1422 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001424 issue = int(issue)
1425 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001426 self.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001427 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001428 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001429 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001430 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001431 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001432 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 else:
tandrii5d48c322016-08-18 16:19:37 -07001434 # Reset all of these just to be clean.
1435 reset_suffixes = [
1436 'last-upload-hash',
Edward Lemur125d60a2019-09-13 18:25:41 +00001437 self.IssueConfigKey(),
1438 self.PatchsetConfigKey(),
1439 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001440 ] + self._PostUnsetIssueProperties()
1441 for prop in reset_suffixes:
1442 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001443 msg = RunGit(['log', '-1', '--format=%B']).strip()
1444 if msg and git_footers.get_footer_change_id(msg):
1445 print('WARNING: The change patched into this branch has a Change-Id. '
1446 'Removing it.')
1447 RunGit(['commit', '--amend', '-m',
1448 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001449 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001451 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452
dnjba1b0f32016-09-02 12:37:42 -07001453 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001454 if not self.GitSanityChecks(upstream_branch):
1455 DieWithError('\nGit sanity check failure')
1456
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001457 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001458 if not root:
1459 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001460 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001461
1462 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001463 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001464 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001466 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 except subprocess2.CalledProcessError:
1468 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001469 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001470 'This branch probably doesn\'t exist anymore. To reset the\n'
1471 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001472 ' git branch --set-upstream-to origin/master %s\n'
1473 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001474 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001475
maruel@chromium.org52424302012-08-29 15:14:30 +00001476 issue = self.GetIssue()
1477 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001478 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001479 description = self.GetDescription()
1480 else:
1481 # If the change was never uploaded, use the log messages of all commits
1482 # up to the branch point, as git cl upload will prefill the description
1483 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001484 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1485 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001486
1487 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001488 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001489 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490 name,
1491 description,
1492 absroot,
1493 files,
1494 issue,
1495 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001496 author,
1497 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001498
dsansomee2d6fd92016-09-08 00:10:47 -07001499 def UpdateDescription(self, description, force=False):
Edward Lemur125d60a2019-09-13 18:25:41 +00001500 self.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001501 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001502 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001503
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001504 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1505 """Sets the description for this CL remotely.
1506
1507 You can get description_lines and footers with GetDescriptionFooters.
1508
1509 Args:
1510 description_lines (list(str)) - List of CL description lines without
1511 newline characters.
1512 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1513 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1514 `List-Of-Tokens`). It will be case-normalized so that each token is
1515 title-cased.
1516 """
1517 new_description = '\n'.join(description_lines)
1518 if footers:
1519 new_description += '\n'
1520 for k, v in footers:
1521 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1522 if not git_footers.FOOTER_PATTERN.match(foot):
1523 raise ValueError('Invalid footer %r' % foot)
1524 new_description += foot + '\n'
1525 self.UpdateDescription(new_description, force)
1526
Edward Lesmes8e282792018-04-03 18:50:29 -04001527 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001528 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1529 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001530 start = time_time()
1531 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1533 default_presubmit=None, may_prompt=may_prompt,
Edward Lemur125d60a2019-09-13 18:25:41 +00001534 gerrit_obj=self.GetGerritObjForPresubmit(),
Edward Lesmes8e282792018-04-03 18:50:29 -04001535 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001536 metrics.collector.add_repeated('sub_commands', {
1537 'command': 'presubmit',
1538 'execution_time': time_time() - start,
1539 'exit_code': 0 if result.should_continue() else 1,
1540 })
1541 return result
vapierfd77ac72016-06-16 08:33:57 -07001542 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001543 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001544
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 def CMDUpload(self, options, git_diff_args, orig_args):
1546 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001547 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001549 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550 else:
1551 if self.GetBranch() is None:
1552 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1553
1554 # Default to diffing against common ancestor of upstream branch
1555 base_branch = self.GetCommonAncestorWithUpstream()
1556 git_diff_args = [base_branch, 'HEAD']
1557
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001558 # Fast best-effort checks to abort before running potentially expensive
1559 # hooks if uploading is likely to fail anyway. Passing these checks does
1560 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001561 self.EnsureAuthenticated(force=options.force)
1562 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001563
1564 # Apply watchlists on upload.
1565 change = self.GetChange(base_branch, None)
1566 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1567 files = [f.LocalPath() for f in change.AffectedFiles()]
1568 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001569 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570
1571 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001572 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573 # Set the reviewer list now so that presubmit checks can access it.
1574 change_description = ChangeDescription(change.FullDescriptionText())
1575 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001576 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001577 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001578 change)
1579 change.SetDescriptionText(change_description.description)
1580 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001581 may_prompt=not options.force,
1582 verbose=options.verbose,
1583 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 if not hook_results.should_continue():
1585 return 1
1586 if not options.reviewers and hook_results.reviewers:
1587 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001588 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589
Aaron Gable13101a62018-02-09 13:20:41 -08001590 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001591 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001593 _git_set_branch_config_value('last-upload-hash',
1594 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001595 # Run post upload hooks, if specified.
1596 if settings.GetRunPostUploadHook():
1597 presubmit_support.DoPostUploadExecuter(
1598 change,
1599 self,
1600 settings.GetRoot(),
1601 options.verbose,
1602 sys.stdout)
1603
1604 # Upload all dependencies if specified.
1605 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001606 print()
1607 print('--dependencies has been specified.')
1608 print('All dependent local branches will be re-uploaded.')
1609 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 # Remove the dependencies flag from args so that we do not end up in a
1611 # loop.
1612 orig_args.remove('--dependencies')
1613 ret = upload_branch_deps(self, orig_args)
1614 return ret
1615
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001616 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001617 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001618
1619 Issue must have been already uploaded and known.
1620 """
1621 assert new_state in _CQState.ALL_STATES
1622 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001623 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001624 vote_map = {
1625 _CQState.NONE: 0,
1626 _CQState.DRY_RUN: 1,
1627 _CQState.COMMIT: 2,
1628 }
1629 labels = {'Commit-Queue': vote_map[new_state]}
1630 notify = False if new_state == _CQState.DRY_RUN else None
1631 gerrit_util.SetReview(
1632 self._GetGerritHost(), self._GerritChangeIdentifier(),
1633 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001634 return 0
1635 except KeyboardInterrupt:
1636 raise
1637 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001638 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001639 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001640 ' * Your project has no CQ,\n'
1641 ' * You don\'t have permission to change the CQ state,\n'
1642 ' * There\'s a bug in this code (see stack trace below).\n'
1643 'Consider specifying which bots to trigger manually or asking your '
1644 'project owners for permissions or contacting Chrome Infra at:\n'
1645 'https://www.chromium.org/infra\n\n' %
1646 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001647 # Still raise exception so that stack trace is printed.
1648 raise
1649
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001650 def _GetGerritHost(self):
1651 # Lazy load of configs.
1652 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001653 if self._gerrit_host and '.' not in self._gerrit_host:
1654 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1655 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001656 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001657 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001658 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001659 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001660 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1661 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001662 return self._gerrit_host
1663
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001664 def _GetGitHost(self):
1665 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001666 remote_url = self.GetRemoteUrl()
1667 if not remote_url:
1668 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001669 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001670
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001671 def GetCodereviewServer(self):
1672 if not self._gerrit_server:
1673 # If we're on a branch then get the server potentially associated
1674 # with that branch.
1675 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001676 self._gerrit_server = self._GitGetBranchConfigValue(
1677 self.CodereviewServerConfigKey())
1678 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001679 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001680 if not self._gerrit_server:
1681 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1682 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001683 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001684 parts[0] = parts[0] + '-review'
1685 self._gerrit_host = '.'.join(parts)
1686 self._gerrit_server = 'https://%s' % self._gerrit_host
1687 return self._gerrit_server
1688
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001689 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001690 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001691 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001692 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001693 logging.warn('can\'t detect Gerrit project.')
1694 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001695 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001696 if project.endswith('.git'):
1697 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001698 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1699 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1700 # gitiles/git-over-https protocol. E.g.,
1701 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1702 # as
1703 # https://chromium.googlesource.com/v8/v8
1704 if project.startswith('a/'):
1705 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001706 return project
1707
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001708 def _GerritChangeIdentifier(self):
1709 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1710
1711 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001712 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001713 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001714 project = self._GetGerritProject()
1715 if project:
1716 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1717 # Fall back on still unique, but less efficient change number.
1718 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001719
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001720 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001721 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001722 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723
tandrii5d48c322016-08-18 16:19:37 -07001724 @classmethod
1725 def PatchsetConfigKey(cls):
1726 return 'gerritpatchset'
1727
1728 @classmethod
1729 def CodereviewServerConfigKey(cls):
1730 return 'gerritserver'
1731
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001732 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001733 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001734 if settings.GetGerritSkipEnsureAuthenticated():
1735 # For projects with unusual authentication schemes.
1736 # See http://crbug.com/603378.
1737 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001738
1739 # Check presence of cookies only if using cookies-based auth method.
1740 cookie_auth = gerrit_util.Authenticator.get()
1741 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001742 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001743
Edward Lemur79d4f992019-11-11 23:49:02 +00001744 if urllib.parse.urlparse(self.GetRemoteUrl()).scheme != 'https':
Daniel Chengcf6269b2019-05-18 01:02:12 +00001745 print('WARNING: Ignoring branch %s with non-https remote %s' %
Edward Lemur125d60a2019-09-13 18:25:41 +00001746 (self.branch, self.GetRemoteUrl()))
Daniel Chengcf6269b2019-05-18 01:02:12 +00001747 return
1748
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001749 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001750 self.GetCodereviewServer()
1751 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001752 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001753
1754 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1755 git_auth = cookie_auth.get_auth_header(git_host)
1756 if gerrit_auth and git_auth:
1757 if gerrit_auth == git_auth:
1758 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001759 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001760 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001761 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001762 ' %s\n'
1763 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001764 ' Consider running the following command:\n'
1765 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001766 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001767 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001768 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001769 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001770 cookie_auth.get_new_password_message(git_host)))
1771 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001772 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001773 return
1774 else:
1775 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001776 ([] if gerrit_auth else [self._gerrit_host]) +
1777 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001778 DieWithError('Credentials for the following hosts are required:\n'
1779 ' %s\n'
1780 'These are read from %s (or legacy %s)\n'
1781 '%s' % (
1782 '\n '.join(missing),
1783 cookie_auth.get_gitcookies_path(),
1784 cookie_auth.get_netrc_path(),
1785 cookie_auth.get_new_password_message(git_host)))
1786
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001787 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001788 if not self.GetIssue():
1789 return
1790
1791 # Warm change details cache now to avoid RPCs later, reducing latency for
1792 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001793 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001794 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001795
1796 status = self._GetChangeDetail()['status']
1797 if status in ('MERGED', 'ABANDONED'):
1798 DieWithError('Change %s has been %s, new uploads are not allowed' %
1799 (self.GetIssueURL(),
1800 'submitted' if status == 'MERGED' else 'abandoned'))
1801
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001802 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1803 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1804 # Apparently this check is not very important? Otherwise get_auth_email
1805 # could have been added to other implementations of Authenticator.
1806 cookies_auth = gerrit_util.Authenticator.get()
1807 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001808 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001809
1810 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001811 if self.GetIssueOwner() == cookies_user:
1812 return
1813 logging.debug('change %s owner is %s, cookies user is %s',
1814 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001815 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001816 # so ask what Gerrit thinks of this user.
1817 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1818 if details['email'] == self.GetIssueOwner():
1819 return
1820 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001821 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001822 'as %s.\n'
1823 'Uploading may fail due to lack of permissions.' %
1824 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1825 confirm_or_exit(action='upload')
1826
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001827 def _PostUnsetIssueProperties(self):
1828 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001829 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001830
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001831 def GetGerritObjForPresubmit(self):
1832 return presubmit_support.GerritAccessor(self._GetGerritHost())
1833
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001834 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001835 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001836 or CQ status, assuming adherence to a common workflow.
1837
1838 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001839 * 'error' - error from review tool (including deleted issues)
1840 * 'unsent' - no reviewers added
1841 * 'waiting' - waiting for review
1842 * 'reply' - waiting for uploader to reply to review
1843 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001844 * 'dry-run' - dry-running in the CQ
1845 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001846 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001847 """
1848 if not self.GetIssue():
1849 return None
1850
1851 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001852 data = self._GetChangeDetail([
1853 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00001854 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001855 return 'error'
1856
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001857 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001858 return 'closed'
1859
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001860 cq_label = data['labels'].get('Commit-Queue', {})
1861 max_cq_vote = 0
1862 for vote in cq_label.get('all', []):
1863 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1864 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001865 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001866 if max_cq_vote == 1:
1867 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001868
Aaron Gable9ab38c62017-04-06 14:36:33 -07001869 if data['labels'].get('Code-Review', {}).get('approved'):
1870 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001871
1872 if not data.get('reviewers', {}).get('REVIEWER', []):
1873 return 'unsent'
1874
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001875 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00001876 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Aaron Gable9ab38c62017-04-06 14:36:33 -07001877 last_message_author = messages.pop().get('author', {})
1878 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001879 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
1880 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07001881 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001882 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07001883 if last_message_author.get('_account_id') == owner:
1884 # Most recent message was by owner.
1885 return 'waiting'
1886 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001887 # Some reply from non-owner.
1888 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001889
1890 # Somehow there are no messages even though there are reviewers.
1891 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001892
1893 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001894 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001895 patchset = data['revisions'][data['current_revision']]['_number']
1896 self.SetPatchset(patchset)
1897 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001898
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001899 def FetchDescription(self, force=False):
1900 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
1901 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00001902 current_rev = data['current_revision']
Edward Lemur79d4f992019-11-11 23:49:02 +00001903 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904
dsansomee2d6fd92016-09-08 00:10:47 -07001905 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001906 if gerrit_util.HasPendingChangeEdit(
1907 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07001908 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001909 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07001910 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001911 'unpublished edit. Either publish the edit in the Gerrit web UI '
1912 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07001913
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001914 gerrit_util.DeletePendingChangeEdit(
1915 self._GetGerritHost(), self._GerritChangeIdentifier())
1916 gerrit_util.SetCommitMessage(
1917 self._GetGerritHost(), self._GerritChangeIdentifier(),
1918 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919
Aaron Gable636b13f2017-07-14 10:42:48 -07001920 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001921 gerrit_util.SetReview(
1922 self._GetGerritHost(), self._GerritChangeIdentifier(),
1923 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001924
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001925 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001926 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001927 # CURRENT_REVISION is included to get the latest patchset so that
1928 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001929 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001930 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1931 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001932 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001933 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001934 robot_file_comments = gerrit_util.GetChangeRobotComments(
1935 self._GetGerritHost(), self._GerritChangeIdentifier())
1936
1937 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001938 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001939 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001940 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001941 line_comments = file_comments.setdefault(path, [])
1942 line_comments.extend(
1943 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001944
1945 # Build dictionary of file comments for easy access and sorting later.
1946 # {author+date: {path: {patchset: {line: url+message}}}}
1947 comments = collections.defaultdict(
1948 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001949 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001950 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001951 tag = comment.get('tag', '')
1952 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001953 continue
1954 key = (comment['author']['email'], comment['updated'])
1955 if comment.get('side', 'REVISION') == 'PARENT':
1956 patchset = 'Base'
1957 else:
1958 patchset = 'PS%d' % comment['patch_set']
1959 line = comment.get('line', 0)
1960 url = ('https://%s/c/%s/%s/%s#%s%s' %
1961 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1962 'b' if comment.get('side') == 'PARENT' else '',
1963 str(line) if line else ''))
1964 comments[key][path][patchset][line] = (url, comment['message'])
1965
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001966 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001967 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001968 summary = self._BuildCommentSummary(msg, comments, readable)
1969 if summary:
1970 summaries.append(summary)
1971 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001972
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001973 @staticmethod
1974 def _BuildCommentSummary(msg, comments, readable):
1975 key = (msg['author']['email'], msg['date'])
1976 # Don't bother showing autogenerated messages that don't have associated
1977 # file or line comments. this will filter out most autogenerated
1978 # messages, but will keep robot comments like those from Tricium.
1979 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1980 if is_autogenerated and not comments.get(key):
1981 return None
1982 message = msg['message']
1983 # Gerrit spits out nanoseconds.
1984 assert len(msg['date'].split('.')[-1]) == 9
1985 date = datetime.datetime.strptime(msg['date'][:-3],
1986 '%Y-%m-%d %H:%M:%S.%f')
1987 if key in comments:
1988 message += '\n'
1989 for path, patchsets in sorted(comments.get(key, {}).items()):
1990 if readable:
1991 message += '\n%s' % path
1992 for patchset, lines in sorted(patchsets.items()):
1993 for line, (url, content) in sorted(lines.items()):
1994 if line:
1995 line_str = 'Line %d' % line
1996 path_str = '%s:%d:' % (path, line)
1997 else:
1998 line_str = 'File comment'
1999 path_str = '%s:0:' % path
2000 if readable:
2001 message += '\n %s, %s: %s' % (patchset, line_str, url)
2002 message += '\n %s\n' % content
2003 else:
2004 message += '\n%s ' % path_str
2005 message += '\n%s\n' % content
2006
2007 return _CommentSummary(
2008 date=date,
2009 message=message,
2010 sender=msg['author']['email'],
2011 autogenerated=is_autogenerated,
2012 # These could be inferred from the text messages and correlated with
2013 # Code-Review label maximum, however this is not reliable.
2014 # Leaving as is until the need arises.
2015 approval=False,
2016 disapproval=False,
2017 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002018
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002019 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002020 gerrit_util.AbandonChange(
2021 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002022
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002023 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002024 gerrit_util.SubmitChange(
2025 self._GetGerritHost(), self._GerritChangeIdentifier(),
2026 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002028 def _GetChangeDetail(self, options=None, no_cache=False):
2029 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002030
2031 If fresh data is needed, set no_cache=True which will clear cache and
2032 thus new data will be fetched from Gerrit.
2033 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002034 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002035 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002036
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002037 # Optimization to avoid multiple RPCs:
2038 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2039 'CURRENT_COMMIT' not in options):
2040 options.append('CURRENT_COMMIT')
2041
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002042 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002043 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002044 options = [o.upper() for o in options]
2045
2046 # Check in cache first unless no_cache is True.
2047 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002048 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002049 else:
2050 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002051 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002052 # Assumption: data fetched before with extra options is suitable
2053 # for return for a smaller set of options.
2054 # For example, if we cached data for
2055 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2056 # and request is for options=[CURRENT_REVISION],
2057 # THEN we can return prior cached data.
2058 if options_set.issubset(cached_options_set):
2059 return data
2060
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002061 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002062 data = gerrit_util.GetChangeDetail(
2063 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002064 except gerrit_util.GerritError as e:
2065 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002066 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002067 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002068
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002069 self._detail_cache.setdefault(cache_key, []).append(
2070 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002071 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002072
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002073 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002074 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002075 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002076 data = gerrit_util.GetChangeCommit(
2077 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002078 except gerrit_util.GerritError as e:
2079 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002080 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002081 raise
agable32978d92016-11-01 12:55:02 -07002082 return data
2083
Karen Qian40c19422019-03-13 21:28:29 +00002084 def _IsCqConfigured(self):
2085 detail = self._GetChangeDetail(['LABELS'])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002086 if u'Commit-Queue' not in detail.get('labels', {}):
Karen Qian40c19422019-03-13 21:28:29 +00002087 return False
2088 # TODO(crbug/753213): Remove temporary hack
2089 if ('https://chromium.googlesource.com/chromium/src' ==
Edward Lemur125d60a2019-09-13 18:25:41 +00002090 self.GetRemoteUrl() and
Karen Qian40c19422019-03-13 21:28:29 +00002091 detail['branch'].startswith('refs/branch-heads/')):
2092 return False
2093 return True
2094
Olivier Robin75ee7252018-04-13 10:02:56 +02002095 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002096 if git_common.is_dirty_git_tree('land'):
2097 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002098
tandriid60367b2016-06-22 05:25:12 -07002099 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002100 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002101 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002102 'which can test and land changes for you. '
2103 'Are you sure you wish to bypass it?\n',
2104 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002105 differs = True
tandriic4344b52016-08-29 06:04:54 -07002106 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002107 # Note: git diff outputs nothing if there is no diff.
2108 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002109 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002110 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002111 if detail['current_revision'] == last_upload:
2112 differs = False
2113 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002114 print('WARNING: Local branch contents differ from latest uploaded '
2115 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002116 if differs:
2117 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002118 confirm_or_exit(
2119 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2120 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002121 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002122 elif not bypass_hooks:
2123 hook_results = self.RunHook(
2124 committing=True,
2125 may_prompt=not force,
2126 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002127 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2128 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002129 if not hook_results.should_continue():
2130 return 1
2131
2132 self.SubmitIssue(wait_for_merge=True)
2133 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002134 links = self._GetChangeCommit().get('web_links', [])
2135 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002136 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002137 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002138 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002139 return 0
2140
Edward Lemurf38bc172019-09-03 21:02:13 +00002141 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002142 assert parsed_issue_arg.valid
2143
Edward Lemur125d60a2019-09-13 18:25:41 +00002144 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145
2146 if parsed_issue_arg.hostname:
2147 self._gerrit_host = parsed_issue_arg.hostname
2148 self._gerrit_server = 'https://%s' % self._gerrit_host
2149
tandriic2405f52016-10-10 08:13:15 -07002150 try:
2151 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002152 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002153 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002154
2155 if not parsed_issue_arg.patchset:
2156 # Use current revision by default.
2157 revision_info = detail['revisions'][detail['current_revision']]
2158 patchset = int(revision_info['_number'])
2159 else:
2160 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002161 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002162 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2163 break
2164 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002165 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002166 (parsed_issue_arg.patchset, self.GetIssue()))
2167
Edward Lemur125d60a2019-09-13 18:25:41 +00002168 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002169 if remote_url.endswith('.git'):
2170 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002171 remote_url = remote_url.rstrip('/')
2172
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002174 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002175
2176 if remote_url != fetch_info['url']:
2177 DieWithError('Trying to patch a change from %s but this repo appears '
2178 'to be %s.' % (fetch_info['url'], remote_url))
2179
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002181
Aaron Gable62619a32017-06-16 08:22:09 -07002182 if force:
2183 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2184 print('Checked out commit for change %i patchset %i locally' %
2185 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002186 elif nocommit:
2187 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2188 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002189 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002190 RunGit(['cherry-pick', 'FETCH_HEAD'])
2191 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002192 (parsed_issue_arg.issue, patchset))
2193 print('Note: this created a local commit which does not have '
2194 'the same hash as the one uploaded for review. This will make '
2195 'uploading changes based on top of this branch difficult.\n'
2196 'If you want to do that, use "git cl patch --force" instead.')
2197
Stefan Zagerd08043c2017-10-12 12:07:02 -07002198 if self.GetBranch():
2199 self.SetIssue(parsed_issue_arg.issue)
2200 self.SetPatchset(patchset)
2201 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2202 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2203 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2204 else:
2205 print('WARNING: You are in detached HEAD state.\n'
2206 'The patch has been applied to your checkout, but you will not be '
2207 'able to upload a new patch set to the gerrit issue.\n'
2208 'Try using the \'-b\' option if you would like to work on a '
2209 'branch and/or upload a new patch set.')
2210
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002211 return 0
2212
tandrii16e0b4e2016-06-07 10:34:28 -07002213 def _GerritCommitMsgHookCheck(self, offer_removal):
2214 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2215 if not os.path.exists(hook):
2216 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002217 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2218 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002219 data = gclient_utils.FileRead(hook)
2220 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2221 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002222 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002223 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002224 'and may interfere with it in subtle ways.\n'
2225 'We recommend you remove the commit-msg hook.')
2226 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002227 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002228 gclient_utils.rm_file_or_tree(hook)
2229 print('Gerrit commit-msg hook removed.')
2230 else:
2231 print('OK, will keep Gerrit commit-msg hook in place.')
2232
Edward Lemur1b52d872019-05-09 21:12:12 +00002233 def _CleanUpOldTraces(self):
2234 """Keep only the last |MAX_TRACES| traces."""
2235 try:
2236 traces = sorted([
2237 os.path.join(TRACES_DIR, f)
2238 for f in os.listdir(TRACES_DIR)
2239 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2240 and not f.startswith('tmp'))
2241 ])
2242 traces_to_delete = traces[:-MAX_TRACES]
2243 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002244 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002245 except OSError:
2246 print('WARNING: Failed to remove old git traces from\n'
2247 ' %s'
2248 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002249
Edward Lemur5737f022019-05-17 01:24:00 +00002250 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002251 """Zip and write the git push traces stored in traces_dir."""
2252 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002253 traces_zip = trace_name + '-traces'
2254 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002255 # Create a temporary dir to store git config and gitcookies in. It will be
2256 # compressed and stored next to the traces.
2257 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002258 git_info_zip = trace_name + '-git-info'
2259
Edward Lemur5737f022019-05-17 01:24:00 +00002260 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002261 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002262 git_push_metadata['now'] = git_push_metadata['now'].decode(
2263 sys.stdin.encoding)
2264
Edward Lemur1b52d872019-05-09 21:12:12 +00002265 git_push_metadata['trace_name'] = trace_name
2266 gclient_utils.FileWrite(
2267 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2268
2269 # Keep only the first 6 characters of the git hashes on the packet
2270 # trace. This greatly decreases size after compression.
2271 packet_traces = os.path.join(traces_dir, 'trace-packet')
2272 if os.path.isfile(packet_traces):
2273 contents = gclient_utils.FileRead(packet_traces)
2274 gclient_utils.FileWrite(
2275 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2276 shutil.make_archive(traces_zip, 'zip', traces_dir)
2277
2278 # Collect and compress the git config and gitcookies.
2279 git_config = RunGit(['config', '-l'])
2280 gclient_utils.FileWrite(
2281 os.path.join(git_info_dir, 'git-config'),
2282 git_config)
2283
2284 cookie_auth = gerrit_util.Authenticator.get()
2285 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2286 gitcookies_path = cookie_auth.get_gitcookies_path()
2287 if os.path.isfile(gitcookies_path):
2288 gitcookies = gclient_utils.FileRead(gitcookies_path)
2289 gclient_utils.FileWrite(
2290 os.path.join(git_info_dir, 'gitcookies'),
2291 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2292 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2293
Edward Lemur1b52d872019-05-09 21:12:12 +00002294 gclient_utils.rmtree(git_info_dir)
2295
2296 def _RunGitPushWithTraces(
2297 self, change_desc, refspec, refspec_opts, git_push_metadata):
2298 """Run git push and collect the traces resulting from the execution."""
2299 # Create a temporary directory to store traces in. Traces will be compressed
2300 # and stored in a 'traces' dir inside depot_tools.
2301 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002302 trace_name = os.path.join(
2303 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002304
2305 env = os.environ.copy()
2306 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2307 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002308 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002309 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2310 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2311 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2312
2313 try:
2314 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002315 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002316 before_push = time_time()
2317 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002318 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002319 env=env,
2320 print_stdout=True,
2321 # Flush after every line: useful for seeing progress when running as
2322 # recipe.
2323 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002324 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002325 except subprocess2.CalledProcessError as e:
2326 push_returncode = e.returncode
2327 DieWithError('Failed to create a change. Please examine output above '
2328 'for the reason of the failure.\n'
2329 'Hint: run command below to diagnose common Git/Gerrit '
2330 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002331 ' git cl creds-check\n'
2332 '\n'
2333 'If git-cl is not working correctly, file a bug under the '
2334 'Infra>SDK component including the files below.\n'
2335 'Review the files before upload, since they might contain '
2336 'sensitive information.\n'
2337 'Set the Restrict-View-Google label so that they are not '
2338 'publicly accessible.\n'
2339 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002340 change_desc)
2341 finally:
2342 execution_time = time_time() - before_push
2343 metrics.collector.add_repeated('sub_commands', {
2344 'command': 'git push',
2345 'execution_time': execution_time,
2346 'exit_code': push_returncode,
2347 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2348 })
2349
Edward Lemur1b52d872019-05-09 21:12:12 +00002350 git_push_metadata['execution_time'] = execution_time
2351 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002352 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002353
Edward Lemur1b52d872019-05-09 21:12:12 +00002354 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002355 gclient_utils.rmtree(traces_dir)
2356
2357 return push_stdout
2358
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002359 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002360 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002361 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002362 # Load default for user, repo, squash=true, in this order.
2363 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002364
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002365 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002366 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002367 # This may be None; default fallback value is determined in logic below.
2368 title = options.title
2369
Dominic Battre7d1c4842017-10-27 09:17:28 +02002370 # Extract bug number from branch name.
2371 bug = options.bug
Dan Beamd8b04ca2019-10-10 21:23:26 +00002372 fixed = options.fixed
2373 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
2374 self.GetBranch())
2375 if not bug and not fixed and match:
2376 if match.group('type') == 'bug':
2377 bug = match.group('bugnum')
2378 else:
2379 fixed = match.group('bugnum')
Dominic Battre7d1c4842017-10-27 09:17:28 +02002380
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002381 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002382 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002383 if self.GetIssue():
2384 # Try to get the message from a previous upload.
2385 message = self.GetDescription()
2386 if not message:
2387 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002388 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002389 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002390 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002391 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002392 # When uploading a subsequent patchset, -m|--message is taken
2393 # as the patchset title if --title was not provided.
2394 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002395 else:
2396 default_title = RunGit(
2397 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002398 if options.force:
2399 title = default_title
2400 else:
2401 title = ask_for_data(
2402 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002403 change_id = self._GetChangeDetail()['change_id']
2404 while True:
2405 footer_change_ids = git_footers.get_footer_change_id(message)
2406 if footer_change_ids == [change_id]:
2407 break
2408 if not footer_change_ids:
2409 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002410 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002411 continue
2412 # There is already a valid footer but with different or several ids.
2413 # Doing this automatically is non-trivial as we don't want to lose
2414 # existing other footers, yet we want to append just 1 desired
2415 # Change-Id. Thus, just create a new footer, but let user verify the
2416 # new description.
2417 message = '%s\n\nChange-Id: %s' % (message, change_id)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002418 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002419 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002420 print(
2421 'WARNING: change %s has Change-Id footer(s):\n'
2422 ' %s\n'
2423 'but change has Change-Id %s, according to Gerrit.\n'
2424 'Please, check the proposed correction to the description, '
2425 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2426 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2427 change_id))
2428 confirm_or_exit(action='edit')
2429 change_desc.prompt()
2430
2431 message = change_desc.description
2432 if not message:
2433 DieWithError("Description is empty. Aborting...")
2434
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002435 # Continue the while loop.
2436 # Sanity check of this code - we should end up with proper message
2437 # footer.
2438 assert [change_id] == git_footers.get_footer_change_id(message)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002439 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
Aaron Gableb56ad332017-01-06 15:24:31 -08002440 else: # if not self.GetIssue()
2441 if options.message:
2442 message = options.message
2443 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002444 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002445 if options.title:
2446 message = options.title + '\n\n' + message
Dan Beamd8b04ca2019-10-10 21:23:26 +00002447 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002448 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002449 change_desc.prompt()
2450
Aaron Gableb56ad332017-01-06 15:24:31 -08002451 # On first upload, patchset title is always this string, while
2452 # --title flag gets converted to first line of message.
2453 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002454 if not change_desc.description:
2455 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002456 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002457 if len(change_ids) > 1:
2458 DieWithError('too many Change-Id footers, at most 1 allowed.')
2459 if not change_ids:
2460 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002461 change_desc.set_description(git_footers.add_footer_change_id(
2462 change_desc.description,
2463 GenerateGerritChangeId(change_desc.description)))
2464 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002465 assert len(change_ids) == 1
2466 change_id = change_ids[0]
2467
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002468 if options.reviewers or options.tbrs or options.add_owners_to:
2469 change_desc.update_reviewers(options.reviewers, options.tbrs,
2470 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002471 if options.preserve_tryjobs:
2472 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002473
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002474 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002475 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2476 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002477 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lesmesf6a22322019-11-04 22:14:39 +00002478 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
Edward Lemur79d4f992019-11-11 23:49:02 +00002479 desc_tempfile.write(change_desc.description.encode('utf-8', 'replace'))
Aaron Gable9a03ae02017-11-03 11:31:07 -07002480 desc_tempfile.close()
2481 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2482 '-F', desc_tempfile.name]).strip()
2483 os.remove(desc_tempfile.name)
Anthony Polito8b955342019-09-24 19:01:36 +00002484 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002486 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002487 if not change_desc.description:
2488 DieWithError("Description is empty. Aborting...")
2489
2490 if not git_footers.get_footer_change_id(change_desc.description):
2491 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002492 change_desc.set_description(
2493 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002494 if options.reviewers or options.tbrs or options.add_owners_to:
2495 change_desc.update_reviewers(options.reviewers, options.tbrs,
2496 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002497 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002498 # For no-squash mode, we assume the remote called "origin" is the one we
2499 # want. It is not worthwhile to support different workflows for
2500 # no-squash mode.
2501 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002502 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2503
2504 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002505 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002506 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2507 ref_to_push)]).splitlines()
2508 if len(commits) > 1:
2509 print('WARNING: This will upload %d commits. Run the following command '
2510 'to see which commits will be uploaded: ' % len(commits))
2511 print('git log %s..%s' % (parent, ref_to_push))
2512 print('You can also use `git squash-branch` to squash these into a '
2513 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002514 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002515
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002516 if options.reviewers or options.tbrs or options.add_owners_to:
2517 change_desc.update_reviewers(options.reviewers, options.tbrs,
2518 options.add_owners_to, change)
2519
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002520 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002521 cc = []
2522 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2523 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2524 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002525 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002526 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002527 if options.cc:
2528 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002529 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002530 if change_desc.get_cced():
2531 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002532 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2533 valid_accounts = set(reviewers + cc)
2534 # TODO(crbug/877717): relax this for all hosts.
2535 else:
2536 valid_accounts = gerrit_util.ValidAccounts(
2537 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002538 logging.info('accounts %s are recognized, %s invalid',
2539 sorted(valid_accounts),
2540 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002541
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002542 # Extra options that can be specified at push time. Doc:
2543 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002544 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002545
Aaron Gable844cf292017-06-28 11:32:59 -07002546 # By default, new changes are started in WIP mode, and subsequent patchsets
2547 # don't send email. At any time, passing --send-mail will mark the change
2548 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002549 if options.send_mail:
2550 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002551 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002552 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002553 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002554 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002555 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002556
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002557 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002558 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002559
Aaron Gable9b713dd2016-12-14 16:04:21 -08002560 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002561 # Punctuation and whitespace in |title| must be percent-encoded.
2562 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002563
agablec6787972016-09-09 16:13:34 -07002564 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002565 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002566
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002567 for r in sorted(reviewers):
2568 if r in valid_accounts:
2569 refspec_opts.append('r=%s' % r)
2570 reviewers.remove(r)
2571 else:
2572 # TODO(tandrii): this should probably be a hard failure.
2573 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2574 % r)
2575 for c in sorted(cc):
2576 # refspec option will be rejected if cc doesn't correspond to an
2577 # account, even though REST call to add such arbitrary cc may succeed.
2578 if c in valid_accounts:
2579 refspec_opts.append('cc=%s' % c)
2580 cc.remove(c)
2581
rmistry9eadede2016-09-19 11:22:43 -07002582 if options.topic:
2583 # Documentation on Gerrit topics is here:
2584 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002585 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002586
Edward Lemur687ca902018-12-05 02:30:30 +00002587 if options.enable_auto_submit:
2588 refspec_opts.append('l=Auto-Submit+1')
2589 if options.use_commit_queue:
2590 refspec_opts.append('l=Commit-Queue+2')
2591 elif options.cq_dry_run:
2592 refspec_opts.append('l=Commit-Queue+1')
2593
2594 if change_desc.get_reviewers(tbr_only=True):
2595 score = gerrit_util.GetCodeReviewTbrScore(
2596 self._GetGerritHost(),
2597 self._GetGerritProject())
2598 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002599
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002600 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002601 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002602 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002603 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002604 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2605
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002606 refspec_suffix = ''
2607 if refspec_opts:
2608 refspec_suffix = '%' + ','.join(refspec_opts)
2609 assert ' ' not in refspec_suffix, (
2610 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2611 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2612
Edward Lemur1b52d872019-05-09 21:12:12 +00002613 git_push_metadata = {
2614 'gerrit_host': self._GetGerritHost(),
2615 'title': title or '<untitled>',
2616 'change_id': change_id,
2617 'description': change_desc.description,
2618 }
2619 push_stdout = self._RunGitPushWithTraces(
2620 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002621
2622 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002623 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002624 change_numbers = [m.group(1)
2625 for m in map(regex.match, push_stdout.splitlines())
2626 if m]
2627 if len(change_numbers) != 1:
2628 DieWithError(
2629 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002630 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002631 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002632 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002633
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002634 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002635 # GetIssue() is not set in case of non-squash uploads according to tests.
2636 # TODO(agable): non-squash uploads in git cl should be removed.
2637 gerrit_util.AddReviewers(
2638 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002639 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002640 reviewers, cc,
2641 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002642
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002643 return 0
2644
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002645 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2646 change_desc):
2647 """Computes parent of the generated commit to be uploaded to Gerrit.
2648
2649 Returns revision or a ref name.
2650 """
2651 if custom_cl_base:
2652 # Try to avoid creating additional unintended CLs when uploading, unless
2653 # user wants to take this risk.
2654 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2655 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2656 local_ref_of_target_remote])
2657 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002658 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002659 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2660 'If you proceed with upload, more than 1 CL may be created by '
2661 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2662 'If you are certain that specified base `%s` has already been '
2663 'uploaded to Gerrit as another CL, you may proceed.\n' %
2664 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2665 if not force:
2666 confirm_or_exit(
2667 'Do you take responsibility for cleaning up potential mess '
2668 'resulting from proceeding with upload?',
2669 action='upload')
2670 return custom_cl_base
2671
Aaron Gablef97e33d2017-03-30 15:44:27 -07002672 if remote != '.':
2673 return self.GetCommonAncestorWithUpstream()
2674
2675 # If our upstream branch is local, we base our squashed commit on its
2676 # squashed version.
2677 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2678
Aaron Gablef97e33d2017-03-30 15:44:27 -07002679 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002680 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002681
2682 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002683 # TODO(tandrii): consider checking parent change in Gerrit and using its
2684 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2685 # the tree hash of the parent branch. The upside is less likely bogus
2686 # requests to reupload parent change just because it's uploadhash is
2687 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002688 parent = RunGit(['config',
2689 'branch.%s.gerritsquashhash' % upstream_branch_name],
2690 error_ok=True).strip()
2691 # Verify that the upstream branch has been uploaded too, otherwise
2692 # Gerrit will create additional CLs when uploading.
2693 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2694 RunGitSilent(['rev-parse', parent + ':'])):
2695 DieWithError(
2696 '\nUpload upstream branch %s first.\n'
2697 'It is likely that this branch has been rebased since its last '
2698 'upload, so you just need to upload it again.\n'
2699 '(If you uploaded it with --no-squash, then branch dependencies '
2700 'are not supported, and you should reupload with --squash.)'
2701 % upstream_branch_name,
2702 change_desc)
2703 return parent
2704
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002705 def _AddChangeIdToCommitMessage(self, options, args):
2706 """Re-commits using the current message, assumes the commit hook is in
2707 place.
2708 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002709 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002710 git_command = ['commit', '--amend', '-m', log_desc]
2711 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002712 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002713 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002714 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002715 return new_log_desc
2716 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002717 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002718
tandriie113dfd2016-10-11 10:20:12 -07002719 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002720 try:
2721 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002722 except GerritChangeNotExists:
2723 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002724
2725 if data['status'] in ('ABANDONED', 'MERGED'):
2726 return 'CL %s is closed' % self.GetIssue()
2727
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002728 def GetGerritChange(self, patchset=None):
2729 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00002730 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002731 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002732 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002733 data = self._GetChangeDetail(['ALL_REVISIONS'])
2734
2735 assert host and issue and patchset, 'CL must be uploaded first'
2736
2737 has_patchset = any(
2738 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002739 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002740 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002741 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002742 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002743
tandrii8c5a3532016-11-04 07:52:02 -07002744 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002745 'host': host,
2746 'change': issue,
2747 'project': data['project'],
2748 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002749 }
tandriie113dfd2016-10-11 10:20:12 -07002750
tandriide281ae2016-10-12 06:02:30 -07002751 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002752 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002753
Edward Lemur707d70b2018-02-07 00:50:14 +01002754 def GetReviewers(self):
2755 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002756 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002757
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002758
2759_CODEREVIEW_IMPLEMENTATIONS = {
Edward Lemur125d60a2019-09-13 18:25:41 +00002760 'gerrit': Changelist,
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002761}
2762
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002763
iannuccie53c9352016-08-17 14:40:40 -07002764def _add_codereview_issue_select_options(parser, extra=""):
2765 _add_codereview_select_options(parser)
2766
2767 text = ('Operate on this issue number instead of the current branch\'s '
2768 'implicit issue.')
2769 if extra:
2770 text += ' '+extra
2771 parser.add_option('-i', '--issue', type=int, help=text)
2772
2773
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002774def _add_codereview_select_options(parser):
Edward Lemurf38bc172019-09-03 21:02:13 +00002775 """Appends --gerrit option to force specific codereview."""
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002776 parser.codereview_group = optparse.OptionGroup(
Edward Lemurf38bc172019-09-03 21:02:13 +00002777 parser, 'DEPRECATED! Codereview override options')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002778 parser.add_option_group(parser.codereview_group)
2779 parser.codereview_group.add_option(
2780 '--gerrit', action='store_true',
Edward Lemurf38bc172019-09-03 21:02:13 +00002781 help='Deprecated. Noop. Do not use.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002782
2783
tandriif9aefb72016-07-01 09:06:51 -07002784def _get_bug_line_values(default_project, bugs):
2785 """Given default_project and comma separated list of bugs, yields bug line
2786 values.
2787
2788 Each bug can be either:
2789 * a number, which is combined with default_project
2790 * string, which is left as is.
2791
2792 This function may produce more than one line, because bugdroid expects one
2793 project per line.
2794
2795 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2796 ['v8:123', 'chromium:789']
2797 """
2798 default_bugs = []
2799 others = []
2800 for bug in bugs.split(','):
2801 bug = bug.strip()
2802 if bug:
2803 try:
2804 default_bugs.append(int(bug))
2805 except ValueError:
2806 others.append(bug)
2807
2808 if default_bugs:
2809 default_bugs = ','.join(map(str, default_bugs))
2810 if default_project:
2811 yield '%s:%s' % (default_project, default_bugs)
2812 else:
2813 yield default_bugs
2814 for other in sorted(others):
2815 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2816 yield other
2817
2818
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002819class ChangeDescription(object):
2820 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002821 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002822 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002823 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002824 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002825 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002826 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2827 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2828 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2829 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002830
Dan Beamd8b04ca2019-10-10 21:23:26 +00002831 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002832 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002833 if bug:
2834 regexp = re.compile(self.BUG_LINE)
2835 prefix = settings.GetBugPrefix()
2836 if not any((regexp.match(line) for line in self._description_lines)):
2837 values = list(_get_bug_line_values(prefix, bug))
2838 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002839 if fixed:
2840 regexp = re.compile(self.FIXED_LINE)
2841 prefix = settings.GetBugPrefix()
2842 if not any((regexp.match(line) for line in self._description_lines)):
2843 values = list(_get_bug_line_values(prefix, fixed))
2844 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002845
agable@chromium.org42c20792013-09-12 17:34:49 +00002846 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002847 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002848 return '\n'.join(self._description_lines)
2849
2850 def set_description(self, desc):
2851 if isinstance(desc, basestring):
2852 lines = desc.splitlines()
2853 else:
2854 lines = [line.rstrip() for line in desc]
2855 while lines and not lines[0]:
2856 lines.pop(0)
2857 while lines and not lines[-1]:
2858 lines.pop(-1)
2859 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002860
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002861 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2862 """Rewrites the R=/TBR= line(s) as a single line each.
2863
2864 Args:
2865 reviewers (list(str)) - list of additional emails to use for reviewers.
2866 tbrs (list(str)) - list of additional emails to use for TBRs.
2867 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2868 the change that are missing OWNER coverage. If this is not None, you
2869 must also pass a value for `change`.
2870 change (Change) - The Change that should be used for OWNERS lookups.
2871 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002872 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002873 assert isinstance(tbrs, list), tbrs
2874
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002875 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002876 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002877
2878 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002879 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002880
2881 reviewers = set(reviewers)
2882 tbrs = set(tbrs)
2883 LOOKUP = {
2884 'TBR': tbrs,
2885 'R': reviewers,
2886 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002887
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002888 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002889 regexp = re.compile(self.R_LINE)
2890 matches = [regexp.match(line) for line in self._description_lines]
2891 new_desc = [l for i, l in enumerate(self._description_lines)
2892 if not matches[i]]
2893 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002894
agable@chromium.org42c20792013-09-12 17:34:49 +00002895 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002896
2897 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002898 for match in matches:
2899 if not match:
2900 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002901 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2902
2903 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002904 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002905 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02002906 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002907 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002908 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002909 LOOKUP[add_owners_to].update(
2910 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002911
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002912 # If any folks ended up in both groups, remove them from tbrs.
2913 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002914
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002915 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2916 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002917
2918 # Put the new lines in the description where the old first R= line was.
2919 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2920 if 0 <= line_loc < len(self._description_lines):
2921 if new_tbr_line:
2922 self._description_lines.insert(line_loc, new_tbr_line)
2923 if new_r_line:
2924 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002925 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002926 if new_r_line:
2927 self.append_footer(new_r_line)
2928 if new_tbr_line:
2929 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002930
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002931 def set_preserve_tryjobs(self):
2932 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2933 footers = git_footers.parse_footers(self.description)
2934 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2935 if v.lower() == 'true':
2936 return
2937 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2938
Anthony Polito8b955342019-09-24 19:01:36 +00002939 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002940 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002941 self.set_description([
2942 '# Enter a description of the change.',
2943 '# This will be displayed on the codereview site.',
2944 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002945 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002946 '--------------------',
2947 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002948 bug_regexp = re.compile(self.BUG_LINE)
2949 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002950 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002951 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2952 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002953 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002954
agable@chromium.org42c20792013-09-12 17:34:49 +00002955 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00002956 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002957 if not content:
2958 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002959 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002960
Bruce Dawson2377b012018-01-11 16:46:49 -08002961 # Strip off comments and default inserted "Bug:" line.
2962 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002963 (line.startswith('#') or
2964 line.rstrip() == "Bug:" or
2965 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002966 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002967 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002968 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002969
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002970 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002971 """Adds a footer line to the description.
2972
2973 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2974 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2975 that Gerrit footers are always at the end.
2976 """
2977 parsed_footer_line = git_footers.parse_footer(line)
2978 if parsed_footer_line:
2979 # Line is a gerrit footer in the form: Footer-Key: any value.
2980 # Thus, must be appended observing Gerrit footer rules.
2981 self.set_description(
2982 git_footers.add_footer(self.description,
2983 key=parsed_footer_line[0],
2984 value=parsed_footer_line[1]))
2985 return
2986
2987 if not self._description_lines:
2988 self._description_lines.append(line)
2989 return
2990
2991 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2992 if gerrit_footers:
2993 # git_footers.split_footers ensures that there is an empty line before
2994 # actual (gerrit) footers, if any. We have to keep it that way.
2995 assert top_lines and top_lines[-1] == ''
2996 top_lines, separator = top_lines[:-1], top_lines[-1:]
2997 else:
2998 separator = [] # No need for separator if there are no gerrit_footers.
2999
3000 prev_line = top_lines[-1] if top_lines else ''
3001 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3002 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3003 top_lines.append('')
3004 top_lines.append(line)
3005 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003006
tandrii99a72f22016-08-17 14:33:24 -07003007 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003008 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003009 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003010 reviewers = [match.group(2).strip()
3011 for match in matches
3012 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003013 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003014
bradnelsond975b302016-10-23 12:20:23 -07003015 def get_cced(self):
3016 """Retrieves the list of reviewers."""
3017 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3018 cced = [match.group(2).strip() for match in matches if match]
3019 return cleanup_list(cced)
3020
Nodir Turakulov23b82142017-11-16 11:04:25 -08003021 def get_hash_tags(self):
3022 """Extracts and sanitizes a list of Gerrit hashtags."""
3023 subject = (self._description_lines or ('',))[0]
3024 subject = re.sub(
3025 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3026
3027 tags = []
3028 start = 0
3029 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3030 while True:
3031 m = bracket_exp.match(subject, start)
3032 if not m:
3033 break
3034 tags.append(self.sanitize_hash_tag(m.group(1)))
3035 start = m.end()
3036
3037 if not tags:
3038 # Try "Tag: " prefix.
3039 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3040 if m:
3041 tags.append(self.sanitize_hash_tag(m.group(1)))
3042 return tags
3043
3044 @classmethod
3045 def sanitize_hash_tag(cls, tag):
3046 """Returns a sanitized Gerrit hash tag.
3047
3048 A sanitized hashtag can be used as a git push refspec parameter value.
3049 """
3050 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3051
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003052 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3053 """Updates this commit description given the parent.
3054
3055 This is essentially what Gnumbd used to do.
3056 Consult https://goo.gl/WMmpDe for more details.
3057 """
3058 assert parent_msg # No, orphan branch creation isn't supported.
3059 assert parent_hash
3060 assert dest_ref
3061 parent_footer_map = git_footers.parse_footers(parent_msg)
3062 # This will also happily parse svn-position, which GnumbD is no longer
3063 # supporting. While we'd generate correct footers, the verifier plugin
3064 # installed in Gerrit will block such commit (ie git push below will fail).
3065 parent_position = git_footers.get_position(parent_footer_map)
3066
3067 # Cherry-picks may have last line obscuring their prior footers,
3068 # from git_footers perspective. This is also what Gnumbd did.
3069 cp_line = None
3070 if (self._description_lines and
3071 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3072 cp_line = self._description_lines.pop()
3073
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003074 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003075
3076 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3077 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003078 for i, line in enumerate(footer_lines):
3079 k, v = git_footers.parse_footer(line) or (None, None)
3080 if k and k.startswith('Cr-'):
3081 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003082
3083 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003084 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003085 if parent_position[0] == dest_ref:
3086 # Same branch as parent.
3087 number = int(parent_position[1]) + 1
3088 else:
3089 number = 1 # New branch, and extra lineage.
3090 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3091 int(parent_position[1])))
3092
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003093 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3094 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003095
3096 self._description_lines = top_lines
3097 if cp_line:
3098 self._description_lines.append(cp_line)
3099 if self._description_lines[-1] != '':
3100 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003101 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003102
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003103
Aaron Gablea1bab272017-04-11 16:38:18 -07003104def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003105 """Retrieves the reviewers that approved a CL from the issue properties with
3106 messages.
3107
3108 Note that the list may contain reviewers that are not committer, thus are not
3109 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003110
3111 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003112 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003113 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003114 return sorted(
3115 set(
3116 message['sender']
3117 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003118 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003119 )
3120 )
3121
3122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003123def FindCodereviewSettingsFile(filename='codereview.settings'):
3124 """Finds the given file starting in the cwd and going up.
3125
3126 Only looks up to the top of the repository unless an
3127 'inherit-review-settings-ok' file exists in the root of the repository.
3128 """
3129 inherit_ok_file = 'inherit-review-settings-ok'
3130 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003131 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003132 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3133 root = '/'
3134 while True:
3135 if filename in os.listdir(cwd):
3136 if os.path.isfile(os.path.join(cwd, filename)):
3137 return open(os.path.join(cwd, filename))
3138 if cwd == root:
3139 break
3140 cwd = os.path.dirname(cwd)
3141
3142
3143def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003144 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003145 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003147 def SetProperty(name, setting, unset_error_ok=False):
3148 fullname = 'rietveld.' + name
3149 if setting in keyvals:
3150 RunGit(['config', fullname, keyvals[setting]])
3151 else:
3152 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3153
tandrii48df5812016-10-17 03:55:37 -07003154 if not keyvals.get('GERRIT_HOST', False):
3155 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003156 # Only server setting is required. Other settings can be absent.
3157 # In that case, we ignore errors raised during option deletion attempt.
3158 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3159 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3160 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003161 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003162 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3163 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003164 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3165 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003166 SetProperty(
3167 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003168
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003169 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003170 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003171
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003172 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003173 RunGit(['config', 'gerrit.squash-uploads',
3174 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003175
tandrii@chromium.org28253532016-04-14 13:46:56 +00003176 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003177 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003178 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3179
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003180 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003181 # should be of the form
3182 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3183 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003184 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3185 keyvals['ORIGIN_URL_CONFIG']])
3186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003187
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003188def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003189 """Downloads a network object to a local file, like urllib.urlretrieve.
3190
3191 This is necessary because urllib is broken for SSL connections via a proxy.
3192 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003193 with open(destination, 'w') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003194 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003195
3196
ukai@chromium.org712d6102013-11-27 00:52:58 +00003197def hasSheBang(fname):
3198 """Checks fname is a #! script."""
3199 with open(fname) as f:
3200 return f.read(2).startswith('#!')
3201
3202
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003203# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3204def DownloadHooks(*args, **kwargs):
3205 pass
3206
3207
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003208def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003209 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003210
3211 Args:
3212 force: True to update hooks. False to install hooks if not present.
3213 """
3214 if not settings.GetIsGerrit():
3215 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003216 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003217 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3218 if not os.access(dst, os.X_OK):
3219 if os.path.exists(dst):
3220 if not force:
3221 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003222 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003223 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003224 if not hasSheBang(dst):
3225 DieWithError('Not a script: %s\n'
3226 'You need to download from\n%s\n'
3227 'into .git/hooks/commit-msg and '
3228 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003229 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3230 except Exception:
3231 if os.path.exists(dst):
3232 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003233 DieWithError('\nFailed to download hooks.\n'
3234 'You need to download from\n%s\n'
3235 'into .git/hooks/commit-msg and '
3236 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003237
3238
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003239class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003240 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003241
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003242 _GOOGLESOURCE = 'googlesource.com'
3243
3244 def __init__(self):
3245 # Cached list of [host, identity, source], where source is either
3246 # .gitcookies or .netrc.
3247 self._all_hosts = None
3248
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003249 def ensure_configured_gitcookies(self):
3250 """Runs checks and suggests fixes to make git use .gitcookies from default
3251 path."""
3252 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3253 configured_path = RunGitSilent(
3254 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003255 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003256 if configured_path:
3257 self._ensure_default_gitcookies_path(configured_path, default)
3258 else:
3259 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003260
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003261 @staticmethod
3262 def _ensure_default_gitcookies_path(configured_path, default_path):
3263 assert configured_path
3264 if configured_path == default_path:
3265 print('git is already configured to use your .gitcookies from %s' %
3266 configured_path)
3267 return
3268
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003269 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003270 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3271 (configured_path, default_path))
3272
3273 if not os.path.exists(configured_path):
3274 print('However, your configured .gitcookies file is missing.')
3275 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3276 action='reconfigure')
3277 RunGit(['config', '--global', 'http.cookiefile', default_path])
3278 return
3279
3280 if os.path.exists(default_path):
3281 print('WARNING: default .gitcookies file already exists %s' %
3282 default_path)
3283 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3284 default_path)
3285
3286 confirm_or_exit('Move existing .gitcookies to default location?',
3287 action='move')
3288 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003289 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003290 print('Moved and reconfigured git to use .gitcookies from %s' %
3291 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003292
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003293 @staticmethod
3294 def _configure_gitcookies_path(default_path):
3295 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3296 if os.path.exists(netrc_path):
3297 print('You seem to be using outdated .netrc for git credentials: %s' %
3298 netrc_path)
3299 print('This tool will guide you through setting up recommended '
3300 '.gitcookies store for git credentials.\n'
3301 '\n'
3302 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3303 ' git config --global --unset http.cookiefile\n'
3304 ' mv %s %s.backup\n\n' % (default_path, default_path))
3305 confirm_or_exit(action='setup .gitcookies')
3306 RunGit(['config', '--global', 'http.cookiefile', default_path])
3307 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003308
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003309 def get_hosts_with_creds(self, include_netrc=False):
3310 if self._all_hosts is None:
3311 a = gerrit_util.CookiesAuthenticator()
3312 self._all_hosts = [
3313 (h, u, s)
3314 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003315 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
3316 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003317 )
3318 if h.endswith(self._GOOGLESOURCE)
3319 ]
3320
3321 if include_netrc:
3322 return self._all_hosts
3323 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3324
3325 def print_current_creds(self, include_netrc=False):
3326 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3327 if not hosts:
3328 print('No Git/Gerrit credentials found')
3329 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003330 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003331 header = [('Host', 'User', 'Which file'),
3332 ['=' * l for l in lengths]]
3333 for row in (header + hosts):
3334 print('\t'.join((('%%+%ds' % l) % s)
3335 for l, s in zip(lengths, row)))
3336
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003337 @staticmethod
3338 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003339 """Parses identity "git-<username>.domain" into <username> and domain."""
3340 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003341 # distinguishable from sub-domains. But we do know typical domains:
3342 if identity.endswith('.chromium.org'):
3343 domain = 'chromium.org'
3344 username = identity[:-len('.chromium.org')]
3345 else:
3346 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003347 if username.startswith('git-'):
3348 username = username[len('git-'):]
3349 return username, domain
3350
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003351 def _canonical_git_googlesource_host(self, host):
3352 """Normalizes Gerrit hosts (with '-review') to Git host."""
3353 assert host.endswith(self._GOOGLESOURCE)
3354 # Prefix doesn't include '.' at the end.
3355 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3356 if prefix.endswith('-review'):
3357 prefix = prefix[:-len('-review')]
3358 return prefix + '.' + self._GOOGLESOURCE
3359
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003360 def _canonical_gerrit_googlesource_host(self, host):
3361 git_host = self._canonical_git_googlesource_host(host)
3362 prefix = git_host.split('.', 1)[0]
3363 return prefix + '-review.' + self._GOOGLESOURCE
3364
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003365 def _get_counterpart_host(self, host):
3366 assert host.endswith(self._GOOGLESOURCE)
3367 git = self._canonical_git_googlesource_host(host)
3368 gerrit = self._canonical_gerrit_googlesource_host(git)
3369 return git if gerrit == host else gerrit
3370
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003371 def has_generic_host(self):
3372 """Returns whether generic .googlesource.com has been configured.
3373
3374 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3375 """
3376 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3377 if host == '.' + self._GOOGLESOURCE:
3378 return True
3379 return False
3380
3381 def _get_git_gerrit_identity_pairs(self):
3382 """Returns map from canonic host to pair of identities (Git, Gerrit).
3383
3384 One of identities might be None, meaning not configured.
3385 """
3386 host_to_identity_pairs = {}
3387 for host, identity, _ in self.get_hosts_with_creds():
3388 canonical = self._canonical_git_googlesource_host(host)
3389 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3390 idx = 0 if canonical == host else 1
3391 pair[idx] = identity
3392 return host_to_identity_pairs
3393
3394 def get_partially_configured_hosts(self):
3395 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003396 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003397 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003398 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003399
3400 def get_conflicting_hosts(self):
3401 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003402 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003403 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003404 if None not in (i1, i2) and i1 != i2)
3405
3406 def get_duplicated_hosts(self):
3407 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003408 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003409
3410 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3411 'chromium.googlesource.com': 'chromium.org',
3412 'chrome-internal.googlesource.com': 'google.com',
3413 }
3414
3415 def get_hosts_with_wrong_identities(self):
3416 """Finds hosts which **likely** reference wrong identities.
3417
3418 Note: skips hosts which have conflicting identities for Git and Gerrit.
3419 """
3420 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003421 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003422 pair = self._get_git_gerrit_identity_pairs().get(host)
3423 if pair and pair[0] == pair[1]:
3424 _, domain = self._parse_identity(pair[0])
3425 if domain != expected:
3426 hosts.add(host)
3427 return hosts
3428
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003429 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003430 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003431 hosts = sorted(hosts)
3432 assert hosts
3433 if extra_column_func is None:
3434 extras = [''] * len(hosts)
3435 else:
3436 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003437 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3438 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003439 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003440 lines.append(tmpl % he)
3441 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003442
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003443 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003444 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003445 yield ('.googlesource.com wildcard record detected',
3446 ['Chrome Infrastructure team recommends to list full host names '
3447 'explicitly.'],
3448 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003449
3450 dups = self.get_duplicated_hosts()
3451 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003452 yield ('The following hosts were defined twice',
3453 self._format_hosts(dups),
3454 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003455
3456 partial = self.get_partially_configured_hosts()
3457 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003458 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3459 'These hosts are missing',
3460 self._format_hosts(partial, lambda host: 'but %s defined' %
3461 self._get_counterpart_host(host)),
3462 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003463
3464 conflicting = self.get_conflicting_hosts()
3465 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003466 yield ('The following Git hosts have differing credentials from their '
3467 'Gerrit counterparts',
3468 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3469 tuple(self._get_git_gerrit_identity_pairs()[host])),
3470 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003471
3472 wrong = self.get_hosts_with_wrong_identities()
3473 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003474 yield ('These hosts likely use wrong identity',
3475 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3476 (self._get_git_gerrit_identity_pairs()[host][0],
3477 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3478 wrong)
3479
3480 def find_and_report_problems(self):
3481 """Returns True if there was at least one problem, else False."""
3482 found = False
3483 bad_hosts = set()
3484 for title, sublines, hosts in self._find_problems():
3485 if not found:
3486 found = True
3487 print('\n\n.gitcookies problem report:\n')
3488 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003489 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003490 if sublines:
3491 print()
3492 print(' %s' % '\n '.join(sublines))
3493 print()
3494
3495 if bad_hosts:
3496 assert found
3497 print(' You can manually remove corresponding lines in your %s file and '
3498 'visit the following URLs with correct account to generate '
3499 'correct credential lines:\n' %
3500 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3501 print(' %s' % '\n '.join(sorted(set(
3502 gerrit_util.CookiesAuthenticator().get_new_password_url(
3503 self._canonical_git_googlesource_host(host))
3504 for host in bad_hosts
3505 ))))
3506 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003507
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003508
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003509@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003510def CMDcreds_check(parser, args):
3511 """Checks credentials and suggests changes."""
3512 _, _ = parser.parse_args(args)
3513
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003514 # Code below checks .gitcookies. Abort if using something else.
3515 authn = gerrit_util.Authenticator.get()
3516 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3517 if isinstance(authn, gerrit_util.GceAuthenticator):
3518 DieWithError(
3519 'This command is not designed for GCE, are you on a bot?\n'
3520 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3521 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003522 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003523 'This command is not designed for bot environment. It checks '
3524 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003525
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003526 checker = _GitCookiesChecker()
3527 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003528
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003529 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003530 checker.print_current_creds(include_netrc=True)
3531
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003532 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003533 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003534 return 0
3535 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003536
3537
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003538@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003539def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003540 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003541 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3542 branch = ShortBranchName(branchref)
3543 _, args = parser.parse_args(args)
3544 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003545 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003546 return RunGit(['config', 'branch.%s.base-url' % branch],
3547 error_ok=False).strip()
3548 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003550 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3551 error_ok=False).strip()
3552
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003553
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003554def color_for_status(status):
3555 """Maps a Changelist status to color, for CMDstatus and other tools."""
3556 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003557 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003558 'waiting': Fore.BLUE,
3559 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003560 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003561 'lgtm': Fore.GREEN,
3562 'commit': Fore.MAGENTA,
3563 'closed': Fore.CYAN,
3564 'error': Fore.WHITE,
3565 }.get(status, Fore.WHITE)
3566
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003567
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003568def get_cl_statuses(changes, fine_grained, max_processes=None):
3569 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003570
3571 If fine_grained is true, this will fetch CL statuses from the server.
3572 Otherwise, simply indicate if there's a matching url for the given branches.
3573
3574 If max_processes is specified, it is used as the maximum number of processes
3575 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3576 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003577
3578 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003579 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003580 if not changes:
3581 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003582
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003583 if not fine_grained:
3584 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003585 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003586 for cl in changes:
3587 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003588 return
3589
3590 # First, sort out authentication issues.
3591 logging.debug('ensuring credentials exist')
3592 for cl in changes:
3593 cl.EnsureAuthenticated(force=False, refresh=True)
3594
3595 def fetch(cl):
3596 try:
3597 return (cl, cl.GetStatus())
3598 except:
3599 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003600 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003601 raise
3602
3603 threads_count = len(changes)
3604 if max_processes:
3605 threads_count = max(1, min(threads_count, max_processes))
3606 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3607
3608 pool = ThreadPool(threads_count)
3609 fetched_cls = set()
3610 try:
3611 it = pool.imap_unordered(fetch, changes).__iter__()
3612 while True:
3613 try:
3614 cl, status = it.next(timeout=5)
3615 except multiprocessing.TimeoutError:
3616 break
3617 fetched_cls.add(cl)
3618 yield cl, status
3619 finally:
3620 pool.close()
3621
3622 # Add any branches that failed to fetch.
3623 for cl in set(changes) - fetched_cls:
3624 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003625
rmistry@google.com2dd99862015-06-22 12:22:18 +00003626
3627def upload_branch_deps(cl, args):
3628 """Uploads CLs of local branches that are dependents of the current branch.
3629
3630 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003631
3632 test1 -> test2.1 -> test3.1
3633 -> test3.2
3634 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003635
3636 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3637 run on the dependent branches in this order:
3638 test2.1, test3.1, test3.2, test2.2, test3.3
3639
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003640 Note: This function does not rebase your local dependent branches. Use it
3641 when you make a change to the parent branch that will not conflict
3642 with its dependent branches, and you would like their dependencies
3643 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003644 """
3645 if git_common.is_dirty_git_tree('upload-branch-deps'):
3646 return 1
3647
3648 root_branch = cl.GetBranch()
3649 if root_branch is None:
3650 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3651 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003652 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003653 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3654 'patchset dependencies without an uploaded CL.')
3655
3656 branches = RunGit(['for-each-ref',
3657 '--format=%(refname:short) %(upstream:short)',
3658 'refs/heads'])
3659 if not branches:
3660 print('No local branches found.')
3661 return 0
3662
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003663 # Create a dictionary of all local branches to the branches that are
3664 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003665 tracked_to_dependents = collections.defaultdict(list)
3666 for b in branches.splitlines():
3667 tokens = b.split()
3668 if len(tokens) == 2:
3669 branch_name, tracked = tokens
3670 tracked_to_dependents[tracked].append(branch_name)
3671
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print()
3673 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003674 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003675
rmistry@google.com2dd99862015-06-22 12:22:18 +00003676 def traverse_dependents_preorder(branch, padding=''):
3677 dependents_to_process = tracked_to_dependents.get(branch, [])
3678 padding += ' '
3679 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003681 dependents.append(dependent)
3682 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003683
rmistry@google.com2dd99862015-06-22 12:22:18 +00003684 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003685 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003686
3687 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003688 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003689 return 0
3690
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003691 confirm_or_exit('This command will checkout all dependent branches and run '
3692 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003693
rmistry@google.com2dd99862015-06-22 12:22:18 +00003694 # Record all dependents that failed to upload.
3695 failures = {}
3696 # Go through all dependents, checkout the branch and upload.
3697 try:
3698 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003699 print()
3700 print('--------------------------------------')
3701 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003702 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003703 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003704 try:
3705 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003706 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003707 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003708 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003709 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003710 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003711 finally:
3712 # Swap back to the original root branch.
3713 RunGit(['checkout', '-q', root_branch])
3714
vapiera7fbd5a2016-06-16 09:17:49 -07003715 print()
3716 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003717 for dependent_branch in dependents:
3718 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003719 print(' %s : %s' % (dependent_branch, upload_status))
3720 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003721
3722 return 0
3723
3724
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003725@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003726def CMDarchive(parser, args):
3727 """Archives and deletes branches associated with closed changelists."""
3728 parser.add_option(
3729 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003730 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003731 parser.add_option(
3732 '-f', '--force', action='store_true',
3733 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003734 parser.add_option(
3735 '-d', '--dry-run', action='store_true',
3736 help='Skip the branch tagging and removal steps.')
3737 parser.add_option(
3738 '-t', '--notags', action='store_true',
3739 help='Do not tag archived branches. '
3740 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003741
kmarshall3bff56b2016-06-06 18:31:47 -07003742 options, args = parser.parse_args(args)
3743 if args:
3744 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003745
3746 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3747 if not branches:
3748 return 0
3749
vapiera7fbd5a2016-06-16 09:17:49 -07003750 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003751 changes = [Changelist(branchref=b)
3752 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003753 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3754 statuses = get_cl_statuses(changes,
3755 fine_grained=True,
3756 max_processes=options.maxjobs)
3757 proposal = [(cl.GetBranch(),
3758 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3759 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003760 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003761 proposal.sort()
3762
3763 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003764 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003765 return 0
3766
3767 current_branch = GetCurrentBranch()
3768
vapiera7fbd5a2016-06-16 09:17:49 -07003769 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003770 if options.notags:
3771 for next_item in proposal:
3772 print(' ' + next_item[0])
3773 else:
3774 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3775 for next_item in proposal:
3776 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003777
kmarshall9249e012016-08-23 12:02:16 -07003778 # Quit now on precondition failure or if instructed by the user, either
3779 # via an interactive prompt or by command line flags.
3780 if options.dry_run:
3781 print('\nNo changes were made (dry run).\n')
3782 return 0
3783 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003784 print('You are currently on a branch \'%s\' which is associated with a '
3785 'closed codereview issue, so archive cannot proceed. Please '
3786 'checkout another branch and run this command again.' %
3787 current_branch)
3788 return 1
kmarshall9249e012016-08-23 12:02:16 -07003789 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003790 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3791 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003792 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003793 return 1
3794
3795 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003796 if not options.notags:
3797 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003798 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003799
vapiera7fbd5a2016-06-16 09:17:49 -07003800 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003801
3802 return 0
3803
3804
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003805@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003807 """Show status of changelists.
3808
3809 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003810 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003811 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003812 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003813 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003814 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003815 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003816 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003817
3818 Also see 'git cl comments'.
3819 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003820 parser.add_option(
3821 '--no-branch-color',
3822 action='store_true',
3823 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003825 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003826 parser.add_option('-f', '--fast', action='store_true',
3827 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003828 parser.add_option(
3829 '-j', '--maxjobs', action='store', type=int,
3830 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003831
iannuccie53c9352016-08-17 14:40:40 -07003832 _add_codereview_issue_select_options(
3833 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003834 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003835 if args:
3836 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003837
iannuccie53c9352016-08-17 14:40:40 -07003838 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003839 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07003840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003841 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003842 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003844 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845 elif options.field == 'id':
3846 issueid = cl.GetIssue()
3847 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003848 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003849 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003850 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003851 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003852 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003853 elif options.field == 'status':
3854 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003855 elif options.field == 'url':
3856 url = cl.GetIssueURL()
3857 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003858 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003859 return 0
3860
3861 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3862 if not branches:
3863 print('No local branch found.')
3864 return 0
3865
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003866 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003867 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003868 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003869 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003870 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003871 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003872 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003873
Daniel McArdlea23bf592019-02-12 00:25:12 +00003874 current_branch = GetCurrentBranch()
3875
3876 def FormatBranchName(branch, colorize=False):
3877 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3878 an asterisk when it is the current branch."""
3879
3880 asterisk = ""
3881 color = Fore.RESET
3882 if branch == current_branch:
3883 asterisk = "* "
3884 color = Fore.GREEN
3885 branch_name = ShortBranchName(branch)
3886
3887 if colorize:
3888 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003889 return asterisk + branch_name
3890
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003891 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003892
3893 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003894 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3895 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003896 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00003897 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003898 branch_statuses[c.GetBranch()] = status
3899 status = branch_statuses.pop(branch)
3900 url = cl.GetIssueURL()
3901 if url and (not status or status == 'error'):
3902 # The issue probably doesn't exist anymore.
3903 url += ' (broken)'
3904
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003905 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003906 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003907 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003908 color = ''
3909 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003910 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003911
Alan Cuttera3be9a52019-03-04 18:50:33 +00003912 branch_display = FormatBranchName(branch)
3913 padding = ' ' * (alignment - len(branch_display))
3914 if not options.no_branch_color:
3915 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003916
Alan Cuttera3be9a52019-03-04 18:50:33 +00003917 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3918 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003919
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003921 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003922 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003923 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003924 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003925 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003926 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003927 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003928 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003929 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print('Issue description:')
3931 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 return 0
3933
3934
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003935def colorize_CMDstatus_doc():
3936 """To be called once in main() to add colors to git cl status help."""
3937 colors = [i for i in dir(Fore) if i[0].isupper()]
3938
3939 def colorize_line(line):
3940 for color in colors:
3941 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003942 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003943 indent = len(line) - len(line.lstrip(' ')) + 1
3944 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3945 return line
3946
3947 lines = CMDstatus.__doc__.splitlines()
3948 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3949
3950
phajdan.jre328cf92016-08-22 04:12:17 -07003951def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003952 if path == '-':
3953 json.dump(contents, sys.stdout)
3954 else:
3955 with open(path, 'w') as f:
3956 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003957
3958
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003959@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003960@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003961def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003962 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003963
3964 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003965 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003966 parser.add_option('-r', '--reverse', action='store_true',
3967 help='Lookup the branch(es) for the specified issues. If '
3968 'no issues are specified, all branches with mapped '
3969 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003970 parser.add_option('--json',
3971 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003972 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003973 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974
dnj@chromium.org406c4402015-03-03 17:22:28 +00003975 if options.reverse:
3976 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003977 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003978 # Reverse issue lookup.
3979 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003980
3981 git_config = {}
3982 for config in RunGit(['config', '--get-regexp',
3983 r'branch\..*issue']).splitlines():
3984 name, _space, val = config.partition(' ')
3985 git_config[name] = val
3986
dnj@chromium.org406c4402015-03-03 17:22:28 +00003987 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003988 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
3989 config_key = _git_branch_config_key(ShortBranchName(branch),
3990 cls.IssueConfigKey())
3991 issue = git_config.get(config_key)
3992 if issue:
3993 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003994 if not args:
3995 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003996 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003997 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003998 try:
3999 issue_num = int(issue)
4000 except ValueError:
4001 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004002 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004003 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004004 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004005 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004006 if options.json:
4007 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004008 return 0
4009
4010 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004011 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004012 if not issue.valid:
4013 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4014 'or no argument to list it.\n'
4015 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004016 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004017 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004018 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004019 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004020 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4021 if options.json:
4022 write_json(options.json, {
4023 'issue': cl.GetIssue(),
4024 'issue_url': cl.GetIssueURL(),
4025 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004026 return 0
4027
4028
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004029@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004030def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004031 """Shows or posts review comments for any changelist."""
4032 parser.add_option('-a', '--add-comment', dest='comment',
4033 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004034 parser.add_option('-p', '--publish', action='store_true',
4035 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004036 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004037 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004038 parser.add_option('-m', '--machine-readable', dest='readable',
4039 action='store_false', default=True,
4040 help='output comments in a format compatible with '
4041 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004042 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004043 help='File to write JSON summary to, or "-" for stdout')
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004044 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004045 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004046
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004047 issue = None
4048 if options.issue:
4049 try:
4050 issue = int(options.issue)
4051 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004052 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004053
Edward Lemur934836a2019-09-09 20:16:54 +00004054 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004055
4056 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004057 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004058 return 0
4059
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004060 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4061 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004062 for comment in summary:
4063 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004064 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004065 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004066 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004067 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004068 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004069 elif comment.autogenerated:
4070 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004071 else:
4072 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004073 print('\n%s%s %s%s\n%s' % (
4074 color,
4075 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4076 comment.sender,
4077 Fore.RESET,
4078 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4079
smut@google.comc85ac942015-09-15 16:34:43 +00004080 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004081 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00004082 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004083 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4084 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00004085 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004086 return 0
4087
4088
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004089@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004090@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004091def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004092 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004093 parser.add_option('-d', '--display', action='store_true',
4094 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004095 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004096 help='New description to set for this issue (- for stdin, '
4097 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004098 parser.add_option('-f', '--force', action='store_true',
4099 help='Delete any unpublished Gerrit edits for this issue '
4100 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004101
4102 _add_codereview_select_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004103 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004104
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004105 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004106 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004107 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004108 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004109 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004110
Edward Lemur934836a2019-09-09 20:16:54 +00004111 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004112 if target_issue_arg:
4113 kwargs['issue'] = target_issue_arg.issue
4114 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004115
4116 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004117 if not cl.GetIssue():
4118 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004119
Edward Lemur678a6842019-10-03 22:25:05 +00004120 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004121 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004122
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004123 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004124
smut@google.com34fb6b12015-07-13 20:03:26 +00004125 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004127 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004128
4129 if options.new_description:
4130 text = options.new_description
4131 if text == '-':
4132 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004133 elif text == '+':
4134 base_branch = cl.GetCommonAncestorWithUpstream()
4135 change = cl.GetChange(base_branch, None, local_description=True)
4136 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004137
4138 description.set_description(text)
4139 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004140 description.prompt()
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004141 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004142 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004143 return 0
4144
4145
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004146@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004147def CMDlint(parser, args):
4148 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004149 parser.add_option('--filter', action='append', metavar='-x,+y',
4150 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004151 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004152
4153 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004154 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004155 try:
4156 import cpplint
4157 import cpplint_chromium
4158 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004159 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004160 return 1
4161
4162 # Change the current working directory before calling lint so that it
4163 # shows the correct base.
4164 previous_cwd = os.getcwd()
4165 os.chdir(settings.GetRoot())
4166 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004167 cl = Changelist()
thestig@chromium.org44202a22014-03-11 19:22:18 +00004168 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4169 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004170 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004171 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004172 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004173
4174 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004175 command = args + files
4176 if options.filter:
4177 command = ['--filter=' + ','.join(options.filter)] + command
4178 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004179
4180 white_regex = re.compile(settings.GetLintRegex())
4181 black_regex = re.compile(settings.GetLintIgnoreRegex())
4182 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4183 for filename in filenames:
4184 if white_regex.match(filename):
4185 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004186 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004187 else:
4188 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4189 extra_check_functions)
4190 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004192 finally:
4193 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004194 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004195 if cpplint._cpplint_state.error_count != 0:
4196 return 1
4197 return 0
4198
4199
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004200@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004201def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004202 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004203 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004204 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004205 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004206 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004207 parser.add_option('--all', action='store_true',
4208 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004209 parser.add_option('--parallel', action='store_true',
4210 help='Run all tests specified by input_api.RunTests in all '
4211 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004212 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213
sbc@chromium.org71437c02015-04-09 19:29:40 +00004214 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004215 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216 return 1
4217
Edward Lemur934836a2019-09-09 20:16:54 +00004218 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219 if args:
4220 base_branch = args[0]
4221 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004222 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004223 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224
Aaron Gable8076c282017-11-29 14:39:41 -08004225 if options.all:
4226 base_change = cl.GetChange(base_branch, None)
4227 files = [('M', f) for f in base_change.AllFiles()]
4228 change = presubmit_support.GitChange(
4229 base_change.Name(),
4230 base_change.FullDescriptionText(),
4231 base_change.RepositoryRoot(),
4232 files,
4233 base_change.issue,
4234 base_change.patchset,
4235 base_change.author_email,
4236 base_change._upstream)
4237 else:
4238 change = cl.GetChange(base_branch, None)
4239
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004240 cl.RunHook(
4241 committing=not options.upload,
4242 may_prompt=False,
4243 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004244 change=change,
4245 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004246 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004247
4248
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004249def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004250 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004251
4252 Works the same way as
4253 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4254 but can be called on demand on all platforms.
4255
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004256 The basic idea is to generate git hash of a state of the tree, original
4257 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004258 """
4259 lines = []
4260 tree_hash = RunGitSilent(['write-tree'])
4261 lines.append('tree %s' % tree_hash.strip())
4262 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4263 if code == 0:
4264 lines.append('parent %s' % parent.strip())
4265 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4266 lines.append('author %s' % author.strip())
4267 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4268 lines.append('committer %s' % committer.strip())
4269 lines.append('')
4270 # Note: Gerrit's commit-hook actually cleans message of some lines and
4271 # whitespace. This code is not doing this, but it clearly won't decrease
4272 # entropy.
4273 lines.append(message)
4274 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004275 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004276 return 'I%s' % change_hash.strip()
4277
4278
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004279def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004280 """Computes the remote branch ref to use for the CL.
4281
4282 Args:
4283 remote (str): The git remote for the CL.
4284 remote_branch (str): The git remote branch for the CL.
4285 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004286 """
4287 if not (remote and remote_branch):
4288 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004289
wittman@chromium.org455dc922015-01-26 20:15:50 +00004290 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004291 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004292 # refs, which are then translated into the remote full symbolic refs
4293 # below.
4294 if '/' not in target_branch:
4295 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4296 else:
4297 prefix_replacements = (
4298 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4299 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4300 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4301 )
4302 match = None
4303 for regex, replacement in prefix_replacements:
4304 match = re.search(regex, target_branch)
4305 if match:
4306 remote_branch = target_branch.replace(match.group(0), replacement)
4307 break
4308 if not match:
4309 # This is a branch path but not one we recognize; use as-is.
4310 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004311 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4312 # Handle the refs that need to land in different refs.
4313 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004314
wittman@chromium.org455dc922015-01-26 20:15:50 +00004315 # Create the true path to the remote branch.
4316 # Does the following translation:
4317 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4318 # * refs/remotes/origin/master -> refs/heads/master
4319 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4320 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4321 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4322 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4323 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4324 'refs/heads/')
4325 elif remote_branch.startswith('refs/remotes/branch-heads'):
4326 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004327
wittman@chromium.org455dc922015-01-26 20:15:50 +00004328 return remote_branch
4329
4330
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004331def cleanup_list(l):
4332 """Fixes a list so that comma separated items are put as individual items.
4333
4334 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4335 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4336 """
4337 items = sum((i.split(',') for i in l), [])
4338 stripped_items = (i.strip() for i in items)
4339 return sorted(filter(None, stripped_items))
4340
4341
Aaron Gable4db38df2017-11-03 14:59:07 -07004342@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004343@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004344def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004345 """Uploads the current changelist to codereview.
4346
4347 Can skip dependency patchset uploads for a branch by running:
4348 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004349 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004350 git config --unset branch.branch_name.skip-deps-uploads
4351 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004352
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004353 If the name of the checked out branch starts with "bug-" or "fix-" followed
4354 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004355 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004356
4357 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004358 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004359 [git-cl] add support for hashtags
4360 Foo bar: implement foo
4361 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004362 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004363 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4364 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004365 parser.add_option('--bypass-watchlists', action='store_true',
4366 dest='bypass_watchlists',
4367 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004368 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004369 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004370 parser.add_option('--message', '-m', dest='message',
4371 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004372 parser.add_option('-b', '--bug',
4373 help='pre-populate the bug number(s) for this issue. '
4374 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004375 parser.add_option('--message-file', dest='message_file',
4376 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004377 parser.add_option('--title', '-t', dest='title',
4378 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004379 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004380 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004381 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004382 parser.add_option('--tbrs',
4383 action='append', default=[],
4384 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004385 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004386 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004387 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004388 parser.add_option('--hashtag', dest='hashtags',
4389 action='append', default=[],
4390 help=('Gerrit hashtag for new CL; '
4391 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004392 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004393 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004394 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004395 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004396 metavar='TARGET',
4397 help='Apply CL to remote ref TARGET. ' +
4398 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004399 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004400 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004401 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004402 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004403 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004404 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004405 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4406 const='TBR', help='add a set of OWNERS to TBR')
4407 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4408 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004409 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004410 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004411 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004412 'implies --send-mail')
4413 parser.add_option('-d', '--cq-dry-run',
4414 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004415 help='Send the patchset to do a CQ dry run right after '
4416 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004417 parser.add_option('--preserve-tryjobs', action='store_true',
4418 help='instruct the CQ to let tryjobs running even after '
4419 'new patchsets are uploaded instead of canceling '
4420 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004421 parser.add_option('--dependencies', action='store_true',
4422 help='Uploads CLs of all the local branches that depend on '
4423 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004424 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4425 help='Sends your change to the CQ after an approval. Only '
4426 'works on repos that have the Auto-Submit label '
4427 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004428 parser.add_option('--parallel', action='store_true',
4429 help='Run all tests specified by input_api.RunTests in all '
4430 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004431 parser.add_option('--no-autocc', action='store_true',
4432 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004433 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004434 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004435 parser.add_option('-R', '--retry-failed', action='store_true',
4436 help='Retry failed tryjobs from old patchset immediately '
4437 'after uploading new patchset. Cannot be used with '
4438 '--use-commit-queue or --cq-dry-run.')
4439 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4440 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004441 parser.add_option('--fixed', '-x',
4442 help='List of bugs that will be commented on and marked '
4443 'fixed (pre-populates "Fixed:" tag). Same format as '
4444 '-b option / "Bug:" tag. If fixing several issues, '
4445 'separate with commas.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004446
rmistry@google.com2dd99862015-06-22 12:22:18 +00004447 orig_args = args
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004448 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004449 (options, args) = parser.parse_args(args)
4450
sbc@chromium.org71437c02015-04-09 19:29:40 +00004451 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004452 return 1
4453
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004454 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004455 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004456 options.cc = cleanup_list(options.cc)
4457
tandriib80458a2016-06-23 12:20:07 -07004458 if options.message_file:
4459 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004460 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004461 options.message = gclient_utils.FileRead(options.message_file)
4462 options.message_file = None
4463
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004464 if ([options.cq_dry_run,
4465 options.use_commit_queue,
4466 options.retry_failed].count(True) > 1):
4467 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4468 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004469
Aaron Gableedbc4132017-09-11 13:22:28 -07004470 if options.use_commit_queue:
4471 options.send_mail = True
4472
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004473 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4474 settings.GetIsGerrit()
4475
Edward Lemur934836a2019-09-09 20:16:54 +00004476 cl = Changelist()
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004477 if options.retry_failed and not cl.GetIssue():
4478 print('No previous patchsets, so --retry-failed has no effect.')
4479 options.retry_failed = False
4480 # cl.GetMostRecentPatchset uses cached information, and can return the last
4481 # patchset before upload. Calling it here makes it clear that it's the
4482 # last patchset before upload. Note that GetMostRecentPatchset will fail
4483 # if no CL has been uploaded yet.
4484 if options.retry_failed:
4485 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004486
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004487 ret = cl.CMDUpload(options, args, orig_args)
4488
4489 if options.retry_failed:
4490 if ret != 0:
4491 print('Upload failed, so --retry-failed has no effect.')
4492 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004493 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004494 cl, options.buildbucket_host, latest_patchset=patchset)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +00004495 buckets = _filter_failed_for_retry(builds)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004496 if len(buckets) == 0:
4497 print('No failed tryjobs, so --retry-failed has no effect.')
4498 return ret
Edward Lemur5b929a42019-10-21 17:57:39 +00004499 _trigger_try_jobs(cl, buckets, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004500
4501 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004502
4503
Francois Dorayd42c6812017-05-30 15:10:20 -04004504@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004505@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004506def CMDsplit(parser, args):
4507 """Splits a branch into smaller branches and uploads CLs.
4508
4509 Creates a branch and uploads a CL for each group of files modified in the
4510 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004511 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004512 the shared OWNERS file.
4513 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004514 parser.add_option('-d', '--description', dest='description_file',
4515 help='A text file containing a CL description in which '
4516 '$directory will be replaced by each CL\'s directory.')
4517 parser.add_option('-c', '--comment', dest='comment_file',
4518 help='A text file containing a CL comment.')
4519 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004520 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004521 help='List the files and reviewers for each CL that would '
4522 'be created, but don\'t create branches or CLs.')
4523 parser.add_option('--cq-dry-run', action='store_true',
4524 help='If set, will do a cq dry run for each uploaded CL. '
4525 'Please be careful when doing this; more than ~10 CLs '
4526 'has the potential to overload our build '
4527 'infrastructure. Try to upload these not during high '
4528 'load times (usually 11-3 Mountain View time). Email '
4529 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004530 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4531 default=True,
4532 help='Sends your change to the CQ after an approval. Only '
4533 'works on repos that have the Auto-Submit label '
4534 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004535 options, _ = parser.parse_args(args)
4536
4537 if not options.description_file:
4538 parser.error('No --description flag specified.')
4539
4540 def WrappedCMDupload(args):
4541 return CMDupload(OptionParser(), args)
4542
4543 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004544 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004545 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004546
4547
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004548@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004549@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004550def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004551 """DEPRECATED: Used to commit the current changelist via git-svn."""
4552 message = ('git-cl no longer supports committing to SVN repositories via '
4553 'git-svn. You probably want to use `git cl land` instead.')
4554 print(message)
4555 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556
4557
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004558@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004559@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004560def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004561 """Commits the current changelist via git.
4562
4563 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4564 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004565 """
4566 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4567 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004568 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004569 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004570 parser.add_option('--parallel', action='store_true',
4571 help='Run all tests specified by input_api.RunTests in all '
4572 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004573 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004574
Edward Lemur934836a2019-09-09 20:16:54 +00004575 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004576
Robert Iannucci2e73d432018-03-14 01:10:47 -07004577 if not cl.GetIssue():
4578 DieWithError('You must upload the change first to Gerrit.\n'
4579 ' If you would rather have `git cl land` upload '
4580 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004581 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004582 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004583
4584
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004585@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004586@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004587def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004588 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589 parser.add_option('-b', dest='newbranch',
4590 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004591 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004592 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004593 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004594 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004595
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004596 group = optparse.OptionGroup(
4597 parser,
4598 'Options for continuing work on the current issue uploaded from a '
4599 'different clone (e.g. different machine). Must be used independently '
4600 'from the other options. No issue number should be specified, and the '
4601 'branch must have an issue number associated with it')
4602 group.add_option('--reapply', action='store_true', dest='reapply',
4603 help='Reset the branch and reapply the issue.\n'
4604 'CAUTION: This will undo any local changes in this '
4605 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004606
4607 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004608 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004609 parser.add_option_group(group)
4610
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004611 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004613
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004614 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004615 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004616 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004617 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004618 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004619
Edward Lemur934836a2019-09-09 20:16:54 +00004620 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004621 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004622 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004623
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004624 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004625 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004626 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004627
4628 RunGit(['reset', '--hard', upstream])
4629 if options.pull:
4630 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004631
Edward Lemur678a6842019-10-03 22:25:05 +00004632 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4633 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004634
4635 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004636 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004637
Edward Lemurf38bc172019-09-03 21:02:13 +00004638 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004639 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004640 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004641
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004642 # We don't want uncommitted changes mixed up with the patch.
4643 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004644 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004645
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004646 if options.newbranch:
4647 if options.force:
4648 RunGit(['branch', '-D', options.newbranch],
4649 stderr=subprocess2.PIPE, error_ok=True)
4650 RunGit(['new-branch', options.newbranch])
4651
Edward Lemur678a6842019-10-03 22:25:05 +00004652 cl = Changelist(
4653 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004654
Edward Lemur678a6842019-10-03 22:25:05 +00004655 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004656 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004657
Edward Lemurf38bc172019-09-03 21:02:13 +00004658 return cl.CMDPatchWithParsedIssue(
4659 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004660
4661
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004662def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004663 """Fetches the tree status and returns either 'open', 'closed',
4664 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004665 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004666 if url:
Edward Lemur79d4f992019-11-11 23:49:02 +00004667 status = urllib.request.urlopen(url).read().lower()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004668 if status.find('closed') != -1 or status == '0':
4669 return 'closed'
4670 elif status.find('open') != -1 or status == '1':
4671 return 'open'
4672 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004673 return 'unset'
4674
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004676def GetTreeStatusReason():
4677 """Fetches the tree status from a json url and returns the message
4678 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004679 url = settings.GetTreeStatusUrl()
4680 json_url = urlparse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00004681 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682 status = json.loads(connection.read())
4683 connection.close()
4684 return status['message']
4685
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004686
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004687@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004688def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004689 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004690 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691 status = GetTreeStatus()
4692 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004693 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004694 return 2
4695
vapiera7fbd5a2016-06-16 09:17:49 -07004696 print('The tree is %s' % status)
4697 print()
4698 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004699 if status != 'open':
4700 return 1
4701 return 0
4702
4703
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004704@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004705def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004706 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4707 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004708 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004709 '-b', '--bot', action='append',
4710 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4711 'times to specify multiple builders. ex: '
4712 '"-b win_rel -b win_layout". See '
4713 'the try server waterfall for the builders name and the tests '
4714 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004715 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004716 '-B', '--bucket', default='',
4717 help=('Buildbucket bucket to send the try requests.'))
4718 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004719 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004720 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004721 'be determined by the try recipe that builder runs, which usually '
4722 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004723 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004724 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004725 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004726 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004727 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004728 '--category', default='git_cl_try', help='Specify custom build category.')
4729 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004730 '--project',
4731 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004732 'in recipe to determine to which repository or directory to '
4733 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004734 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004735 '-p', '--property', dest='properties', action='append', default=[],
4736 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004737 'key2=value2 etc. The value will be treated as '
4738 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004739 'NOTE: using this may make your tryjob not usable for CQ, '
4740 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004741 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004742 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4743 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004744 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004745 parser.add_option(
4746 '-R', '--retry-failed', action='store_true', default=False,
4747 help='Retry failed jobs from the latest set of tryjobs. '
4748 'Not allowed with --bucket and --bot options.')
Koji Ishii31c14782018-01-08 17:17:33 +09004749 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004750 options, args = parser.parse_args(args)
4751
machenbach@chromium.org45453142015-09-15 08:45:22 +00004752 # Make sure that all properties are prop=value pairs.
4753 bad_params = [x for x in options.properties if '=' not in x]
4754 if bad_params:
4755 parser.error('Got properties with missing "=": %s' % bad_params)
4756
maruel@chromium.org15192402012-09-06 12:38:29 +00004757 if args:
4758 parser.error('Unknown arguments: %s' % args)
4759
Edward Lemur934836a2019-09-09 20:16:54 +00004760 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004761 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004762 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004763
Edward Lemurf38bc172019-09-03 21:02:13 +00004764 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004765 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004766
tandriie113dfd2016-10-11 10:20:12 -07004767 error_message = cl.CannotTriggerTryJobReason()
4768 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004769 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004770
Quinten Yearsley983111f2019-09-26 17:18:48 +00004771 if options.retry_failed:
4772 if options.bot or options.bucket:
4773 print('ERROR: The option --retry-failed is not compatible with '
4774 '-B, -b, --bucket, or --bot.', file=sys.stderr)
4775 return 1
4776 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004777 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004778 if options.verbose:
4779 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +00004780 buckets = _filter_failed_for_retry(builds)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004781 if not buckets:
4782 print('There are no failed jobs in the latest set of jobs '
4783 '(patchset #%d), doing nothing.' % patchset)
4784 return 0
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00004785 num_builders = sum(map(len, buckets.values()))
Quinten Yearsley983111f2019-09-26 17:18:48 +00004786 if num_builders > 10:
4787 confirm_or_exit('There are %d builders with failed builds.'
4788 % num_builders, action='continue')
4789 else:
4790 buckets = _get_bucket_map(cl, options, parser)
4791 if buckets and any(b.startswith('master.') for b in buckets):
4792 print('ERROR: Buildbot masters are not supported.')
4793 return 1
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004794
qyearsleydd49f942016-10-28 11:57:22 -07004795 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4796 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004797 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004798 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004799 print('git cl try with no bots now defaults to CQ dry run.')
4800 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4801 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004802
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00004803 for builders in buckets.values():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004804 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004805 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004806 'of bot requires an initial job from a parent (usually a builder). '
4807 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004808 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004809 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004810
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004811 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004812 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00004813 _trigger_try_jobs(cl, buckets, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004814 except BuildbucketResponseException as ex:
4815 print('ERROR: %s' % ex)
4816 return 1
4817 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004818
4819
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004820@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004821def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004822 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004823 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004824 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004825 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004828 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004829 '--color', action='store_true', default=setup_color.IS_TTY,
4830 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004831 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004832 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4833 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004834 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004835 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004836 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004837 parser.add_option_group(group)
Stefan Zager27db3f22017-10-10 15:15:01 -07004838 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004839 options, args = parser.parse_args(args)
4840 if args:
4841 parser.error('Unrecognized args: %s' % ' '.join(args))
4842
Edward Lemur934836a2019-09-09 20:16:54 +00004843 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004844 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004845 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004846
tandrii221ab252016-10-06 08:12:04 -07004847 patchset = options.patchset
4848 if not patchset:
4849 patchset = cl.GetMostRecentPatchset()
4850 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004851 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004852 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004853 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004854 cl.GetIssue())
4855
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004856 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00004857 jobs = fetch_try_jobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004858 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004859 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004860 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004861 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004862 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004863 else:
4864 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004865 return 0
4866
4867
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004868@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004869@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004870def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004871 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004872 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004873 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004874 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004876 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004877 if args:
4878 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004879 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004880 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004881 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004882 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004883
4884 # Clear configured merge-base, if there is one.
4885 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004886 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004887 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004888 return 0
4889
4890
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004891@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004892def CMDweb(parser, args):
4893 """Opens the current CL in the web browser."""
4894 _, args = parser.parse_args(args)
4895 if args:
4896 parser.error('Unrecognized args: %s' % ' '.join(args))
4897
4898 issue_url = Changelist().GetIssueURL()
4899 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004900 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004901 return 1
4902
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004903 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004904 # allows us to hide the "Created new window in existing browser session."
4905 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004906 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004907 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004908 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004909 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004910 os.open(os.devnull, os.O_RDWR)
4911 try:
4912 webbrowser.open(issue_url)
4913 finally:
4914 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004915 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004916 return 0
4917
4918
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004919@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004920def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004921 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004922 parser.add_option('-d', '--dry-run', action='store_true',
4923 help='trigger in dry run mode')
4924 parser.add_option('-c', '--clear', action='store_true',
4925 help='stop CQ run, if any')
iannuccie53c9352016-08-17 14:40:40 -07004926 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004927 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004928 if args:
4929 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004930 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004931 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004932
Edward Lemur934836a2019-09-09 20:16:54 +00004933 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004934 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004935 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004936 elif options.dry_run:
4937 state = _CQState.DRY_RUN
4938 else:
4939 state = _CQState.COMMIT
4940 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004941 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004942 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004943 return 0
4944
4945
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004946@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004947def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004948 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004949 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004950 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004951 if args:
4952 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004953 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004954 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004955 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004956 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004957 cl.CloseIssue()
4958 return 0
4959
4960
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004961@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004962def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004963 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004964 parser.add_option(
4965 '--stat',
4966 action='store_true',
4967 dest='stat',
4968 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004969 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004970 if args:
4971 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004972
Edward Lemur934836a2019-09-09 20:16:54 +00004973 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004974 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004975 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004976 if not issue:
4977 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004978
Aaron Gablea718c3e2017-08-28 17:47:28 -07004979 base = cl._GitGetBranchConfigValue('last-upload-hash')
4980 if not base:
4981 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4982 if not base:
4983 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4984 revision_info = detail['revisions'][detail['current_revision']]
4985 fetch_info = revision_info['fetch']['http']
4986 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4987 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004988
Aaron Gablea718c3e2017-08-28 17:47:28 -07004989 cmd = ['git', 'diff']
4990 if options.stat:
4991 cmd.append('--stat')
4992 cmd.append(base)
4993 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004994
4995 return 0
4996
4997
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004998@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004999def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005000 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005001 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005002 '--ignore-current',
5003 action='store_true',
5004 help='Ignore the CL\'s current reviewers and start from scratch.')
5005 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005006 '--ignore-self',
5007 action='store_true',
5008 help='Do not consider CL\'s author as an owners.')
5009 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005010 '--no-color',
5011 action='store_true',
5012 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005013 parser.add_option(
5014 '--batch',
5015 action='store_true',
5016 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005017 # TODO: Consider moving this to another command, since other
5018 # git-cl owners commands deal with owners for a given CL.
5019 parser.add_option(
5020 '--show-all',
5021 action='store_true',
5022 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005023 options, args = parser.parse_args(args)
5024
5025 author = RunGit(['config', 'user.email']).strip() or None
5026
Edward Lemur934836a2019-09-09 20:16:54 +00005027 cl = Changelist()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005028
Yang Guo6e269a02019-06-26 11:17:02 +00005029 if options.show_all:
5030 for arg in args:
5031 base_branch = cl.GetCommonAncestorWithUpstream()
5032 change = cl.GetChange(base_branch, None)
5033 database = owners.Database(change.RepositoryRoot(), file, os.path)
5034 database.load_data_needed_for([arg])
5035 print('Owners for %s:' % arg)
5036 for owner in sorted(database.all_possible_owners([arg], None)):
5037 print(' - %s' % owner)
5038 return 0
5039
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005040 if args:
5041 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005042 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005043 base_branch = args[0]
5044 else:
5045 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005046 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005047
5048 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005049 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5050
5051 if options.batch:
5052 db = owners.Database(change.RepositoryRoot(), file, os.path)
5053 print('\n'.join(db.reviewers_for(affected_files, author)))
5054 return 0
5055
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005056 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005057 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005058 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005059 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005060 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005061 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005062 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005063 override_files=change.OriginalOwnersFiles(),
5064 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005065
5066
Aiden Bennerc08566e2018-10-03 17:52:42 +00005067def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005068 """Generates a diff command."""
5069 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005070 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5071
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005072 if allow_prefix:
5073 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5074 # case that diff.noprefix is set in the user's git config.
5075 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5076 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005077 diff_cmd += ['--no-prefix']
5078
5079 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005080
5081 if args:
5082 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005083 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005084 diff_cmd.append(arg)
5085 else:
5086 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005087
5088 return diff_cmd
5089
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005090
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005091def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005092 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005093 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005094
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005095
enne@chromium.org555cfe42014-01-29 18:21:39 +00005096@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005097@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005098def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005099 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005100 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005101 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005102 parser.add_option('--full', action='store_true',
5103 help='Reformat the full content of all touched files')
5104 parser.add_option('--dry-run', action='store_true',
5105 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005106 parser.add_option(
5107 '--python',
5108 action='store_true',
5109 default=None,
5110 help='Enables python formatting on all python files.')
5111 parser.add_option(
5112 '--no-python',
5113 action='store_true',
5114 dest='python',
5115 help='Disables python formatting on all python files. '
5116 'Takes precedence over --python. '
5117 'If neither --python or --no-python are set, python '
5118 'files that have a .style.yapf file in an ancestor '
5119 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005120 parser.add_option('--js', action='store_true',
5121 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005122 parser.add_option('--diff', action='store_true',
5123 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005124 parser.add_option('--presubmit', action='store_true',
5125 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005126 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005127
Daniel Chengc55eecf2016-12-30 03:11:02 -08005128 # Normalize any remaining args against the current path, so paths relative to
5129 # the current directory are still resolved as expected.
5130 args = [os.path.join(os.getcwd(), arg) for arg in args]
5131
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005132 # git diff generates paths against the root of the repository. Change
5133 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005134 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005135 if rel_base_path:
5136 os.chdir(rel_base_path)
5137
digit@chromium.org29e47272013-05-17 17:01:46 +00005138 # Grab the merge-base commit, i.e. the upstream commit of the current
5139 # branch when it was created or the last time it was rebased. This is
5140 # to cover the case where the user may have called "git fetch origin",
5141 # moving the origin branch to a newer commit, but hasn't rebased yet.
5142 upstream_commit = None
5143 cl = Changelist()
5144 upstream_branch = cl.GetUpstreamBranch()
5145 if upstream_branch:
5146 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5147 upstream_commit = upstream_commit.strip()
5148
5149 if not upstream_commit:
5150 DieWithError('Could not find base commit for this branch. '
5151 'Are you in detached state?')
5152
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005153 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5154 diff_output = RunGit(changed_files_cmd)
5155 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005156 # Filter out files deleted by this CL
5157 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005158
Christopher Lamc5ba6922017-01-24 11:19:14 +11005159 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005160 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005161
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005162 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5163 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5164 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005165 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005166
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005167 top_dir = os.path.normpath(
5168 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5169
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005170 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5171 # formatted. This is used to block during the presubmit.
5172 return_value = 0
5173
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005174 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005175 # Locate the clang-format binary in the checkout
5176 try:
5177 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005178 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005179 DieWithError(e)
5180
Jamie Madilldc4d19e2019-10-24 21:50:02 +00005181 if opts.full or settings.GetFormatFullByDefault():
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005182 cmd = [clang_format_tool]
5183 if not opts.dry_run and not opts.diff:
5184 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005185 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005186 if opts.diff:
5187 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005188 else:
5189 env = os.environ.copy()
5190 env['PATH'] = str(os.path.dirname(clang_format_tool))
5191 try:
5192 script = clang_format.FindClangFormatScriptInChromiumTree(
5193 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005194 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005195 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005196
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005197 cmd = [sys.executable, script, '-p0']
5198 if not opts.dry_run and not opts.diff:
5199 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005200
Jamie Madill3671a6a2019-10-24 15:13:21 +00005201 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005202 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005203
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005204 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5205 if opts.diff:
5206 sys.stdout.write(stdout)
5207 if opts.dry_run and len(stdout) > 0:
5208 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005209
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005210 # Similar code to above, but using yapf on .py files rather than clang-format
5211 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005212 py_explicitly_disabled = opts.python is not None and not opts.python
5213 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005214 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5215 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5216 if sys.platform.startswith('win'):
5217 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005218
Aiden Bennerc08566e2018-10-03 17:52:42 +00005219 # If we couldn't find a yapf file we'll default to the chromium style
5220 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005221 chromium_default_yapf_style = os.path.join(depot_tools_path,
5222 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005223 # Used for caching.
5224 yapf_configs = {}
5225 for f in python_diff_files:
5226 # Find the yapf style config for the current file, defaults to depot
5227 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005228 _FindYapfConfigFile(f, yapf_configs, top_dir)
5229
5230 # Turn on python formatting by default if a yapf config is specified.
5231 # This breaks in the case of this repo though since the specified
5232 # style file is also the global default.
5233 if opts.python is None:
5234 filtered_py_files = []
5235 for f in python_diff_files:
5236 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5237 filtered_py_files.append(f)
5238 else:
5239 filtered_py_files = python_diff_files
5240
5241 # Note: yapf still seems to fix indentation of the entire file
5242 # even if line ranges are specified.
5243 # See https://github.com/google/yapf/issues/499
5244 if not opts.full and filtered_py_files:
5245 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5246
Brian Sheedy59b06a82019-10-14 17:03:29 +00005247 ignored_yapf_files = _GetYapfIgnoreFilepaths(top_dir)
5248
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005249 for f in filtered_py_files:
Brian Sheedy59b06a82019-10-14 17:03:29 +00005250 if f in ignored_yapf_files:
5251 continue
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005252 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5253 if yapf_config is None:
5254 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005255
5256 cmd = [yapf_tool, '--style', yapf_config, f]
5257
5258 has_formattable_lines = False
5259 if not opts.full:
5260 # Only run yapf over changed line ranges.
5261 for diff_start, diff_len in py_line_diffs[f]:
5262 diff_end = diff_start + diff_len - 1
5263 # Yapf errors out if diff_end < diff_start but this
5264 # is a valid line range diff for a removal.
5265 if diff_end >= diff_start:
5266 has_formattable_lines = True
5267 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5268 # If all line diffs were removals we have nothing to format.
5269 if not has_formattable_lines:
5270 continue
5271
5272 if opts.diff or opts.dry_run:
5273 cmd += ['--diff']
5274 # Will return non-zero exit code if non-empty diff.
5275 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5276 if opts.diff:
5277 sys.stdout.write(stdout)
5278 elif len(stdout) > 0:
5279 return_value = 2
5280 else:
5281 cmd += ['-i']
5282 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005283
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005284 # Dart's formatter does not have the nice property of only operating on
5285 # modified chunks, so hard code full.
5286 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005287 try:
5288 command = [dart_format.FindDartFmtToolInChromiumTree()]
5289 if not opts.dry_run and not opts.diff:
5290 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005291 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005292
ppi@chromium.org6593d932016-03-03 15:41:15 +00005293 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005294 if opts.dry_run and stdout:
5295 return_value = 2
5296 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005297 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5298 'found in this checkout. Files in other languages are still '
5299 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005300
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005301 # Format GN build files. Always run on full build files for canonical form.
5302 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005303 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005304 if opts.dry_run or opts.diff:
5305 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005306 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005307 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5308 shell=sys.platform == 'win32',
5309 cwd=top_dir)
5310 if opts.dry_run and gn_ret == 2:
5311 return_value = 2 # Not formatted.
5312 elif opts.diff and gn_ret == 2:
5313 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005314 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005315 elif gn_ret != 0:
5316 # For non-dry run cases (and non-2 return values for dry-run), a
5317 # nonzero error code indicates a failure, probably because the file
5318 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005319 DieWithError('gn format failed on ' + gn_diff_file +
5320 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005321
Ilya Shermane081cbe2017-08-15 17:51:04 -07005322 # Skip the metrics formatting from the global presubmit hook. These files have
5323 # a separate presubmit hook that issues an error if the files need formatting,
5324 # whereas the top-level presubmit script merely issues a warning. Formatting
5325 # these files is somewhat slow, so it's important not to duplicate the work.
5326 if not opts.presubmit:
5327 for xml_dir in GetDirtyMetricsDirs(diff_files):
5328 tool_dir = os.path.join(top_dir, xml_dir)
5329 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5330 if opts.dry_run or opts.diff:
5331 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005332 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005333 if opts.diff:
5334 sys.stdout.write(stdout)
5335 if opts.dry_run and stdout:
5336 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005337
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005338 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005339
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005340
Steven Holte2e664bf2017-04-21 13:10:47 -07005341def GetDirtyMetricsDirs(diff_files):
5342 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5343 metrics_xml_dirs = [
5344 os.path.join('tools', 'metrics', 'actions'),
5345 os.path.join('tools', 'metrics', 'histograms'),
5346 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005347 os.path.join('tools', 'metrics', 'ukm'),
5348 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005349 for xml_dir in metrics_xml_dirs:
5350 if any(file.startswith(xml_dir) for file in xml_diff_files):
5351 yield xml_dir
5352
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005353
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005354@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005355@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005356def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005357 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005358 _, args = parser.parse_args(args)
5359
5360 if len(args) != 1:
5361 parser.print_help()
5362 return 1
5363
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005364 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005365 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005366 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005367
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005368 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005369
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005370 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005371 output = RunGit(['config', '--local', '--get-regexp',
5372 r'branch\..*\.%s' % issueprefix],
5373 error_ok=True)
5374 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005375 if issue == target_issue:
5376 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005377
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005378 branches = []
5379 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005380 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005381 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005382 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005383 return 1
5384 if len(branches) == 1:
5385 RunGit(['checkout', branches[0]])
5386 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005387 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005388 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005389 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005390 which = raw_input('Choose by index: ')
5391 try:
5392 RunGit(['checkout', branches[int(which)]])
5393 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005394 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005395 return 1
5396
5397 return 0
5398
5399
maruel@chromium.org29404b52014-09-08 22:58:00 +00005400def CMDlol(parser, args):
5401 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005402 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005403 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5404 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5405 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005406 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005407 return 0
5408
5409
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005410class OptionParser(optparse.OptionParser):
5411 """Creates the option parse and add --verbose support."""
5412 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005413 optparse.OptionParser.__init__(
5414 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005415 self.add_option(
5416 '-v', '--verbose', action='count', default=0,
5417 help='Use 2 times for more debugging info')
5418
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005419 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005420 try:
5421 return self._parse_args(args)
5422 finally:
5423 # Regardless of success or failure of args parsing, we want to report
5424 # metrics, but only after logging has been initialized (if parsing
5425 # succeeded).
5426 global settings
5427 settings = Settings()
5428
5429 if not metrics.DISABLE_METRICS_COLLECTION:
5430 # GetViewVCUrl ultimately calls logging method.
5431 project_url = settings.GetViewVCUrl().strip('/+')
5432 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5433 metrics.collector.add('project_urls', [project_url])
5434
5435 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005436 # Create an optparse.Values object that will store only the actual passed
5437 # options, without the defaults.
5438 actual_options = optparse.Values()
5439 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5440 # Create an optparse.Values object with the default options.
5441 options = optparse.Values(self.get_default_values().__dict__)
5442 # Update it with the options passed by the user.
5443 options._update_careful(actual_options.__dict__)
5444 # Store the options passed by the user in an _actual_options attribute.
5445 # We store only the keys, and not the values, since the values can contain
5446 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00005447 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005448
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005449 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005450 logging.basicConfig(
5451 level=levels[min(options.verbose, len(levels) - 1)],
5452 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5453 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005454
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005455 return options, args
5456
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005458def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005459 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005460 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005461 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005462 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005463
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005464 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005465 dispatcher = subcommand.CommandDispatcher(__name__)
5466 try:
5467 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005468 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005469 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00005470 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005471 if e.code != 500:
5472 raise
5473 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005474 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005475 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005476 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005477
5478
5479if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005480 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5481 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005482 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005483 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005484 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005485 sys.exit(main(sys.argv[1:]))