blob: 398e3ebb7955f76252e9be4c91b10d66e54ff318 [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 Lesmesf6a22322019-11-04 22:14:39 +000057if sys.version_info.major == 2:
58 import httplib
59 import urllib2 as urllib_request
60 import urllib2 as urllib_error
61 import urlparse
62else:
63 import http.client as httplib
64 import urllib.request as urllib_request
65 import urllib.error as urllib_error
66 import urllib.parse as urlparse
Edward Lemurb9830242019-10-30 22:19:20 +000067
tandrii7400cf02016-06-21 08:48:07 -070068__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069
Edward Lemur0f58ae42019-04-30 17:24:12 +000070# Traces for git push will be stored in a traces directory inside the
71# depot_tools checkout.
72DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
73TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
74
75# When collecting traces, Git hashes will be reduced to 6 characters to reduce
76# the size after compression.
77GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
78# Used to redact the cookies from the gitcookies file.
79GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
80
Edward Lemurd4d1ba42019-09-20 21:46:37 +000081MAX_ATTEMPTS = 3
82
Edward Lemur1b52d872019-05-09 21:12:12 +000083# The maximum number of traces we will keep. Multiplied by 3 since we store
84# 3 files per trace.
85MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000086# Message to be displayed to the user to inform where to find the traces for a
87# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000088TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000089'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000090'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000091' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000092'Copies of your gitcookies file and git config have been recorded at:\n'
93' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000094# Format of the message to be stored as part of the traces to give developers a
95# better context when they go through traces.
96TRACES_README_FORMAT = (
97'Date: %(now)s\n'
98'\n'
99'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
100'Title: %(title)s\n'
101'\n'
102'%(description)s\n'
103'\n'
104'Execution time: %(execution_time)s\n'
105'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000106
tandrii9d2c7a32016-06-22 03:42:45 -0700107COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800108POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000110REFS_THAT_ALIAS_TO_OTHER_REFS = {
111 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
112 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
113}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
thestig@chromium.org44202a22014-03-11 19:22:18 +0000115# Valid extensions for files we want to lint.
116DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
117DEFAULT_LINT_IGNORE_REGEX = r"$^"
118
Aiden Bennerc08566e2018-10-03 17:52:42 +0000119# File name for yapf style config files.
120YAPF_CONFIG_FILENAME = '.style.yapf'
121
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000122# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000123Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000124
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000125# Initialized in main()
126settings = None
127
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100128# Used by tests/git_cl_test.py to add extra logging.
129# Inside the weirdly failing test, add this:
130# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700131# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100132_IS_BEING_TESTED = False
133
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000134
Christopher Lamf732cd52017-01-24 12:40:11 +1100135def DieWithError(message, change_desc=None):
136 if change_desc:
137 SaveDescriptionBackup(change_desc)
138
vapiera7fbd5a2016-06-16 09:17:49 -0700139 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140 sys.exit(1)
141
142
Christopher Lamf732cd52017-01-24 12:40:11 +1100143def SaveDescriptionBackup(change_desc):
144 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000145 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100146 backup_file = open(backup_path, 'w')
147 backup_file.write(change_desc.description)
148 backup_file.close()
149
150
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000151def GetNoGitPagerEnv():
152 env = os.environ.copy()
153 # 'cat' is a magical git string that disables pagers on all platforms.
154 env['GIT_PAGER'] = 'cat'
155 return env
156
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000157
bsep@chromium.org627d9002016-04-29 00:00:52 +0000158def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000159 try:
Edward Lesmesf6a22322019-11-04 22:14:39 +0000160 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000161 except subprocess2.CalledProcessError as e:
162 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000163 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000165 'Command "%s" failed.\n%s' % (
166 ' '.join(args), error_message or e.stdout or ''))
Edward Lesmesf6a22322019-11-04 22:14:39 +0000167 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000168
169
170def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000171 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000172 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000173
174
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000175def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000176 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700177 if suppress_stderr:
178 stderr = subprocess2.VOID
179 else:
180 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000181 try:
tandrii5d48c322016-08-18 16:19:37 -0700182 (out, _), code = subprocess2.communicate(['git'] + args,
183 env=GetNoGitPagerEnv(),
184 stdout=subprocess2.PIPE,
185 stderr=stderr)
Edward Lesmesf6a22322019-11-04 22:14:39 +0000186 return code, out
tandrii5d48c322016-08-18 16:19:37 -0700187 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900188 logging.debug('Failed running %s', ['git'] + args)
Edward Lesmesf6a22322019-11-04 22:14:39 +0000189 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000190
191
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000192def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000193 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000194 return RunGitWithCode(args, suppress_stderr=True)[1]
195
196
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000197def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000198 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000199 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000200 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000201 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000202
203
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000204def BranchExists(branch):
205 """Return True if specified branch exists."""
206 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
207 suppress_stderr=True)
208 return not code
209
210
tandrii2a16b952016-10-19 07:09:44 -0700211def time_sleep(seconds):
212 # Use this so that it can be mocked in tests without interfering with python
213 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700214 return time.sleep(seconds)
215
216
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000217def time_time():
218 # Use this so that it can be mocked in tests without interfering with python
219 # system machinery.
220 return time.time()
221
222
Edward Lemur1b52d872019-05-09 21:12:12 +0000223def datetime_now():
224 # Use this so that it can be mocked in tests without interfering with python
225 # system machinery.
226 return datetime.datetime.now()
227
228
maruel@chromium.org90541732011-04-01 17:54:18 +0000229def ask_for_data(prompt):
230 try:
231 return raw_input(prompt)
232 except KeyboardInterrupt:
233 # Hide the exception.
234 sys.exit(1)
235
236
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100237def confirm_or_exit(prefix='', action='confirm'):
238 """Asks user to press enter to continue or press Ctrl+C to abort."""
239 if not prefix or prefix.endswith('\n'):
240 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100241 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100242 mid = ' Press'
243 elif prefix.endswith(' '):
244 mid = 'press'
245 else:
246 mid = ' press'
247 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
248
249
250def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000251 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100252 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
253 while True:
254 if 'yes'.startswith(result):
255 return True
256 if 'no'.startswith(result):
257 return False
258 result = ask_for_data('Please, type yes or no: ').lower()
259
260
tandrii5d48c322016-08-18 16:19:37 -0700261def _git_branch_config_key(branch, key):
262 """Helper method to return Git config key for a branch."""
263 assert branch, 'branch name is required to set git config for it'
264 return 'branch.%s.%s' % (branch, key)
265
266
267def _git_get_branch_config_value(key, default=None, value_type=str,
268 branch=False):
269 """Returns git config value of given or current branch if any.
270
271 Returns default in all other cases.
272 """
273 assert value_type in (int, str, bool)
274 if branch is False: # Distinguishing default arg value from None.
275 branch = GetCurrentBranch()
276
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000277 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700278 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000279
tandrii5d48c322016-08-18 16:19:37 -0700280 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700281 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700282 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000283 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700284 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700285 args.append(_git_branch_config_key(branch, key))
286 code, out = RunGitWithCode(args)
287 if code == 0:
288 value = out.strip()
289 if value_type == int:
290 return int(value)
291 if value_type == bool:
292 return bool(value.lower() == 'true')
293 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000294 return default
295
296
tandrii5d48c322016-08-18 16:19:37 -0700297def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000298 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700299
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000300 If value is None, the key will be unset, otherwise it will be set.
301 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700302 """
303 if not branch:
304 branch = GetCurrentBranch()
305 assert branch, 'a branch name OR currently checked out branch is required'
306 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700307 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700308 if value is None:
309 args.append('--unset')
310 elif isinstance(value, bool):
311 args.append('--bool')
312 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700313 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000314 # `git config` also has --int, but apparently git config suffers from
315 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700316 value = str(value)
317 args.append(_git_branch_config_key(branch, key))
318 if value is not None:
319 args.append(value)
320 RunGit(args, **kwargs)
321
322
machenbach@chromium.org45453142015-09-15 08:45:22 +0000323def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000324 prop_list = getattr(options, 'properties', [])
325 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000326 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000327 try:
328 properties[key] = json.loads(val)
329 except ValueError:
330 pass # If a value couldn't be evaluated, treat it as a string.
331 return properties
332
333
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000334# TODO(crbug.com/976104): Remove this function once git-cl try-results has
335# migrated to use buildbucket v2
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000336def _buildbucket_retry(operation_name, http, *args, **kwargs):
337 """Retries requests to buildbucket service and returns parsed json content."""
338 try_count = 0
339 while True:
340 response, content = http.request(*args, **kwargs)
341 try:
342 content_json = json.loads(content)
343 except ValueError:
344 content_json = None
345
346 # Buildbucket could return an error even if status==200.
347 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000348 error = content_json.get('error')
349 if error.get('code') == 403:
350 raise BuildbucketResponseException(
351 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000353 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000354 raise BuildbucketResponseException(msg)
355
356 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700357 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000359 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700360 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000361 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 content)
363 return content_json
364 if response.status < 500 or try_count >= 2:
365 raise httplib2.HttpLib2Error(content)
366
367 # status >= 500 means transient failures.
368 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000369 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370 try_count += 1
371 assert False, 'unreachable'
372
373
Edward Lemur4c707a22019-09-24 21:13:43 +0000374def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000375 """Calls a buildbucket v2 method and returns the parsed json response."""
376 headers = {
377 'Accept': 'application/json',
378 'Content-Type': 'application/json',
379 }
380 request = json.dumps(request)
381 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
382
383 logging.info('POST %s with %s' % (url, request))
384
385 attempts = 1
386 time_to_sleep = 1
387 while True:
388 response, content = http.request(url, 'POST', body=request, headers=headers)
389 if response.status == 200:
390 return json.loads(content[4:])
391 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
392 msg = '%s error when calling POST %s with %s: %s' % (
393 response.status, url, request, content)
394 raise BuildbucketResponseException(msg)
395 logging.debug(
396 '%s error when calling POST %s with %s. '
397 'Sleeping for %d seconds and retrying...' % (
398 response.status, url, request, time_to_sleep))
399 time.sleep(time_to_sleep)
400 time_to_sleep *= 2
401 attempts += 1
402
403 assert False, 'unreachable'
404
405
qyearsley1fdfcb62016-10-24 13:22:03 -0700406def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700407 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000408 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 """
qyearsleydd49f942016-10-28 11:57:22 -0700410 # If no bots are listed, we try to get a set of builders and tests based
411 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700412 if not options.bot:
413 change = changelist.GetChange(
414 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700415 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700416 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700417 change=change,
418 changed_files=change.LocalPaths(),
419 repository_root=settings.GetRoot(),
420 default_presubmit=None,
421 project=None,
422 verbose=options.verbose,
423 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700424 if masters is None:
425 return None
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000426 return {m: b for m, b in masters.items()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700427
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 if options.bucket:
429 return {options.bucket: {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000430 option_parser.error(
431 'Please specify the bucket, e.g. "-B luci.chromium.try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
433
Edward Lemur6215c792019-10-03 21:59:05 +0000434def _parse_bucket(raw_bucket):
435 legacy = True
436 project = bucket = None
437 if '/' in raw_bucket:
438 legacy = False
439 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000440 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000441 elif raw_bucket.startswith('luci.'):
442 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000443 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000444 elif '.' in raw_bucket:
445 project = raw_bucket.split('.')[0]
446 bucket = raw_bucket
447 # Legacy buckets.
448 if legacy:
449 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
450 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000451
452
Edward Lemur5b929a42019-10-21 17:57:39 +0000453def _trigger_try_jobs(changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000454 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700455
456 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000457 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700458 buckets: A nested dict mapping bucket names to builders to tests.
459 options: Command-line options.
460 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000461 print('Scheduling jobs on:')
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000462 for bucket, builders_and_tests in sorted(buckets.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000463 print('Bucket:', bucket)
464 print('\n'.join(
465 ' %s: %s' % (builder, tests)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000466 for builder, tests in sorted(builders_and_tests.items())))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000467 print('To see results here, run: git cl try-results')
468 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700469
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000470 requests = _make_try_job_schedule_requests(
471 changelist, buckets, options, patchset)
472 if not requests:
473 return
474
Edward Lemur5b929a42019-10-21 17:57:39 +0000475 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000476 http.force_exception_to_status_code = True
477
478 batch_request = {'requests': requests}
479 batch_response = _call_buildbucket(
480 http, options.buildbucket_host, 'Batch', batch_request)
481
482 errors = [
483 ' ' + response['error']['message']
484 for response in batch_response.get('responses', [])
485 if 'error' in response
486 ]
487 if errors:
488 raise BuildbucketResponseException(
489 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
490
491
492def _make_try_job_schedule_requests(changelist, buckets, options, patchset):
Edward Lemurf0faf482019-09-25 20:40:17 +0000493 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000494 shared_properties = {'category': getattr(options, 'category', 'git_cl_try')}
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000495 if getattr(options, 'clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000496 shared_properties['clobber'] = True
497 shared_properties.update(_get_properties_from_options(options) or {})
498
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000499 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
500 if options.retry_failed:
501 shared_tags.append({'key': 'retry_failed',
502 'value': '1'})
503
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000504 requests = []
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000505 for raw_bucket, builders_and_tests in sorted(buckets.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000506 project, bucket = _parse_bucket(raw_bucket)
507 if not project or not bucket:
508 print('WARNING Could not parse bucket "%s". Skipping.' % raw_bucket)
509 continue
510
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000511 for builder, tests in sorted(builders_and_tests.items()):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000512 properties = shared_properties.copy()
513 if 'presubmit' in builder.lower():
514 properties['dry_run'] = 'true'
515 if tests:
516 properties['testfilter'] = tests
517
518 requests.append({
519 'scheduleBuild': {
520 'requestId': str(uuid.uuid4()),
521 'builder': {
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000522 'project': getattr(options, 'project', None) or project,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000523 'bucket': bucket,
524 'builder': builder,
525 },
526 'gerritChanges': gerrit_changes,
527 'properties': properties,
528 'tags': [
529 {'key': 'builder', 'value': builder},
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000530 ] + shared_tags,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000531 }
532 })
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000533 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000534
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000535
Edward Lemur5b929a42019-10-21 17:57:39 +0000536def fetch_try_jobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000537 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000539 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000541 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000542 request = {
543 'predicate': {
544 'gerritChanges': [changelist.GetGerritChange(patchset)],
545 },
546 'fields': ','.join('builds.*.' + field for field in fields),
547 }
tandrii221ab252016-10-06 08:12:04 -0700548
Edward Lemur5b929a42019-10-21 17:57:39 +0000549 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 if authenticator.has_cached_credentials():
551 http = authenticator.authorize(httplib2.Http())
552 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700553 print('Warning: Some results might be missing because %s' %
554 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000555 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 http.force_exception_to_status_code = True
558
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000559 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
560 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561
Edward Lemur5b929a42019-10-21 17:57:39 +0000562def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000563 """Fetches builds from the latest patchset that has builds (within
564 the last few patchsets).
565
566 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000567 changelist (Changelist): The CL to fetch builds for
568 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000569 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
570 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000571 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000572 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
573 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000574 """
575 assert buildbucket_host
576 assert changelist.GetIssue(), 'CL must be uploaded first'
577 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000578 if latest_patchset is None:
579 assert changelist.GetMostRecentPatchset()
580 ps = changelist.GetMostRecentPatchset()
581 else:
582 assert latest_patchset > 0, latest_patchset
583 ps = latest_patchset
584
Quinten Yearsley983111f2019-09-26 17:18:48 +0000585 min_ps = max(1, ps - 5)
586 while ps >= min_ps:
Edward Lemur5b929a42019-10-21 17:57:39 +0000587 builds = fetch_try_jobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000588 if len(builds):
589 return builds, ps
590 ps -= 1
591 return [], 0
592
593
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000594def _filter_failed_for_retry(all_builds):
595 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000596
597 Args:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000598 all_builds (list): Builds, in the format returned by fetch_try_jobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000599 i.e. a list of buildbucket.v2.Builds which includes status and builder
600 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000601
602 Returns:
603 A dict of bucket to builder to tests (empty list). This is the same format
604 accepted by _trigger_try_jobs and returned by _get_bucket_map.
605 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000606
607 def _builder_of(build):
608 builder = build['builder']
609 return (builder['project'], builder['bucket'], builder['builder'])
610
611 res = collections.defaultdict(dict)
612 ordered = sorted(all_builds, key=lambda b: (_builder_of(b), b['createTime']))
613 for (proj, buck, bldr), builds in itertools.groupby(ordered, key=_builder_of):
614 # If builder had several builds, retry only if the last one failed.
615 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
616 # build, but in case of retrying failed jobs retrying a flaky one makes
617 # sense.
618 builds = list(builds)
619 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
620 continue
621 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
622 for t in builds[-1]['tags']):
623 # Don't retry experimental build previously triggered by CQ.
624 continue
625 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
626 # Don't retry if any are running.
627 continue
628 res[proj + '/' + buck][bldr] = []
629 return res
Quinten Yearsley983111f2019-09-26 17:18:48 +0000630
631
qyearsleyeab3c042016-08-24 09:18:28 -0700632def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633 """Prints nicely result of fetch_try_jobs."""
634 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000635 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000636 return
637
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000638 longest_builder = max(len(b['builder']['builder']) for b in builds)
639 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000641 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
642 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000643
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000644 builds_by_status = {}
645 for b in builds:
646 builds_by_status.setdefault(b['status'], []).append({
647 'id': b['id'],
648 'name': name_fmt.format(
649 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
650 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000651
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000652 sort_key = lambda b: (b['name'], b['id'])
653
654 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000655 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000656 if not builds:
657 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000658
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000659 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000660 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000661 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000662 else:
663 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
664
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000665 print(colorize(title))
666 for b in sorted(builds, key=sort_key):
667 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000668
669 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000670 print_builds(
671 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
672 print_builds(
673 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
674 color=Fore.MAGENTA)
675 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
676 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
677 color=Fore.MAGENTA)
678 print_builds('Started:', builds_by_status.pop('STARTED', []))
679 print_builds(
680 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000681 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000682 print_builds(
683 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000684 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000685
686
Aiden Bennerc08566e2018-10-03 17:52:42 +0000687def _ComputeDiffLineRanges(files, upstream_commit):
688 """Gets the changed line ranges for each file since upstream_commit.
689
690 Parses a git diff on provided files and returns a dict that maps a file name
691 to an ordered list of range tuples in the form (start_line, count).
692 Ranges are in the same format as a git diff.
693 """
694 # If files is empty then diff_output will be a full diff.
695 if len(files) == 0:
696 return {}
697
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000698 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000699 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000700 diff_output = RunGit(diff_cmd)
701
702 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
703 # 2 capture groups
704 # 0 == fname of diff file
705 # 1 == 'diff_start,diff_count' or 'diff_start'
706 # will match each of
707 # diff --git a/foo.foo b/foo.py
708 # @@ -12,2 +14,3 @@
709 # @@ -12,2 +17 @@
710 # running re.findall on the above string with pattern will give
711 # [('foo.py', ''), ('', '14,3'), ('', '17')]
712
713 curr_file = None
714 line_diffs = {}
715 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
716 if match[0] != '':
717 # Will match the second filename in diff --git a/a.py b/b.py.
718 curr_file = match[0]
719 line_diffs[curr_file] = []
720 else:
721 # Matches +14,3
722 if ',' in match[1]:
723 diff_start, diff_count = match[1].split(',')
724 else:
725 # Single line changes are of the form +12 instead of +12,1.
726 diff_start = match[1]
727 diff_count = 1
728
729 diff_start = int(diff_start)
730 diff_count = int(diff_count)
731
732 # If diff_count == 0 this is a removal we can ignore.
733 line_diffs[curr_file].append((diff_start, diff_count))
734
735 return line_diffs
736
737
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000738def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000739 """Checks if a yapf file is in any parent directory of fpath until top_dir.
740
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000741 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000742 is found returns None. Uses yapf_config_cache as a cache for previously found
743 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000744 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000745 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000746 # Return result if we've already computed it.
747 if fpath in yapf_config_cache:
748 return yapf_config_cache[fpath]
749
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000750 parent_dir = os.path.dirname(fpath)
751 if os.path.isfile(fpath):
752 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000753 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000754 # Otherwise fpath is a directory
755 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
756 if os.path.isfile(yapf_file):
757 ret = yapf_file
758 elif fpath == top_dir or parent_dir == fpath:
759 # If we're at the top level directory, or if we're at root
760 # there is no provided style.
761 ret = None
762 else:
763 # Otherwise recurse on the current directory.
764 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000765 yapf_config_cache[fpath] = ret
766 return ret
767
768
Brian Sheedy59b06a82019-10-14 17:03:29 +0000769def _GetYapfIgnoreFilepaths(top_dir):
770 """Returns all filepaths that match the ignored files in the .yapfignore file.
771
772 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
773 but this functionality appears to break when explicitly passing files to
774 yapf for formatting. According to
775 https://github.com/google/yapf/blob/master/README.rst#excluding-files-from-formatting-yapfignore,
776 the .yapfignore file should be in the directory that yapf is invoked from,
777 which we assume to be the top level directory in this case.
778
779 Args:
780 top_dir: The top level directory for the repository being formatted.
781
782 Returns:
783 A set of all filepaths that should be ignored by yapf.
784 """
785 yapfignore_file = os.path.join(top_dir, '.yapfignore')
786 ignore_filepaths = set()
787 if not os.path.exists(yapfignore_file):
788 return ignore_filepaths
789
790 # glob works relative to the current working directory, so we need to ensure
791 # that we're at the top level directory.
792 old_cwd = os.getcwd()
793 try:
794 os.chdir(top_dir)
795 with open(yapfignore_file) as f:
796 for line in f.readlines():
797 stripped_line = line.strip()
798 # Comments and blank lines should be ignored.
799 if stripped_line.startswith('#') or stripped_line == '':
800 continue
801 ignore_filepaths |= set(glob.glob(stripped_line))
802 return ignore_filepaths
803 finally:
804 os.chdir(old_cwd)
805
806
Aaron Gable13101a62018-02-09 13:20:41 -0800807def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000808 """Prints statistics about the change to the user."""
809 # --no-ext-diff is broken in some versions of Git, so try to work around
810 # this by overriding the environment (but there is still a problem if the
811 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000813 if 'GIT_EXTERNAL_DIFF' in env:
814 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000815
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000816 try:
817 stdout = sys.stdout.fileno()
818 except AttributeError:
819 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000820 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800821 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000822 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000823
824
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000825class BuildbucketResponseException(Exception):
826 pass
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829class Settings(object):
830 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000832 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 self.tree_status_url = None
834 self.viewvc_url = None
835 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000836 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000837 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000838 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000839 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000840 self.format_full_by_default = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
842 def LazyUpdateIfNeeded(self):
843 """Updates the settings from a codereview.settings file, if available."""
844 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000845 # The only value that actually changes the behavior is
846 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000847 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000848 error_ok=True
849 ).strip().lower()
850
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000852 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853 LoadCodereviewSettingsFromFile(cr_settings_file)
854 self.updated = True
855
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000856 @staticmethod
857 def GetRelativeRoot():
858 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000861 if self.root is None:
862 self.root = os.path.abspath(self.GetRelativeRoot())
863 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 def GetTreeStatusUrl(self, error_ok=False):
866 if not self.tree_status_url:
867 error_message = ('You must configure your tree status URL by running '
868 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000869 self.tree_status_url = self._GetConfig(
870 'rietveld.tree-status-url', error_ok=error_ok,
871 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 return self.tree_status_url
873
874 def GetViewVCUrl(self):
875 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000876 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 return self.viewvc_url
878
rmistry@google.com90752582014-01-14 21:04:50 +0000879 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000880 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000881
rmistry@google.com5626a922015-02-26 14:03:30 +0000882 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000883 run_post_upload_hook = self._GetConfig(
884 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000885 return run_post_upload_hook == "True"
886
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000887 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000888 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000889
ukai@chromium.orge8077812012-02-03 03:41:46 +0000890 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000891 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000892 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700893 self.is_gerrit = (
894 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000895 return self.is_gerrit
896
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000897 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000898 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000899 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700900 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
901 if self.squash_gerrit_uploads is None:
902 # Default is squash now (http://crbug.com/611892#c23).
903 self.squash_gerrit_uploads = not (
904 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
905 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000906 return self.squash_gerrit_uploads
907
tandriia60502f2016-06-20 02:01:53 -0700908 def GetSquashGerritUploadsOverride(self):
909 """Return True or False if codereview.settings should be overridden.
910
911 Returns None if no override has been defined.
912 """
913 # See also http://crbug.com/611892#c23
914 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
915 error_ok=True).strip()
916 if result == 'true':
917 return True
918 if result == 'false':
919 return False
920 return None
921
tandrii@chromium.org28253532016-04-14 13:46:56 +0000922 def GetGerritSkipEnsureAuthenticated(self):
923 """Return True if EnsureAuthenticated should not be done for Gerrit
924 uploads."""
925 if self.gerrit_skip_ensure_authenticated is None:
926 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000927 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000928 error_ok=True).strip() == 'true')
929 return self.gerrit_skip_ensure_authenticated
930
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000931 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000932 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000933 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000934 # Git requires single quotes for paths with spaces. We need to replace
935 # them with double quotes for Windows to treat such paths as a single
936 # path.
937 self.git_editor = self._GetConfig(
938 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000939 return self.git_editor or None
940
thestig@chromium.org44202a22014-03-11 19:22:18 +0000941 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000942 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000943 DEFAULT_LINT_REGEX)
944
945 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000946 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000947 DEFAULT_LINT_IGNORE_REGEX)
948
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000949 def GetFormatFullByDefault(self):
950 if self.format_full_by_default is None:
951 result = (
952 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
953 error_ok=True).strip())
954 self.format_full_by_default = (result == 'true')
955 return self.format_full_by_default
956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 def _GetConfig(self, param, **kwargs):
958 self.LazyUpdateIfNeeded()
959 return RunGit(['config', param], **kwargs).strip()
960
961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962def ShortBranchName(branch):
963 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000964 return branch.replace('refs/heads/', '', 1)
965
966
967def GetCurrentBranchRef():
968 """Returns branch ref (e.g., refs/heads/master) or None."""
969 return RunGit(['symbolic-ref', 'HEAD'],
970 stderr=subprocess2.VOID, error_ok=True).strip() or None
971
972
973def GetCurrentBranch():
974 """Returns current branch or None.
975
976 For refs/heads/* branches, returns just last part. For others, full ref.
977 """
978 branchref = GetCurrentBranchRef()
979 if branchref:
980 return ShortBranchName(branchref)
981 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982
983
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000984class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000985 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000986 NONE = 'none'
987 DRY_RUN = 'dry_run'
988 COMMIT = 'commit'
989
990 ALL_STATES = [NONE, DRY_RUN, COMMIT]
991
992
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000993class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000994 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000995 self.issue = issue
996 self.patchset = patchset
997 self.hostname = hostname
998
999 @property
1000 def valid(self):
1001 return self.issue is not None
1002
1003
Edward Lemurf38bc172019-09-03 21:02:13 +00001004def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001005 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1006 fail_result = _ParsedIssueNumberArgument()
1007
Edward Lemur678a6842019-10-03 22:25:05 +00001008 if isinstance(arg, int):
1009 return _ParsedIssueNumberArgument(issue=arg)
1010 if not isinstance(arg, basestring):
1011 return fail_result
1012
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001013 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00001014 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001015 if not arg.startswith('http'):
1016 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001017
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001018 url = gclient_utils.UpgradeToHttps(arg)
1019 try:
Edward Lesmesf6a22322019-11-04 22:14:39 +00001020 parsed_url = urlparse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001021 except ValueError:
1022 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001023
Edward Lemur678a6842019-10-03 22:25:05 +00001024 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
1025 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
1026 # Short urls like https://domain/<issue_number> can be used, but don't allow
1027 # specifying the patchset (you'd 404), but we allow that here.
1028 if parsed_url.path == '/':
1029 part = parsed_url.fragment
1030 else:
1031 part = parsed_url.path
1032
1033 match = re.match(
1034 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
1035 if not match:
1036 return fail_result
1037
1038 issue = int(match.group('issue'))
1039 patchset = match.group('patchset')
1040 return _ParsedIssueNumberArgument(
1041 issue=issue,
1042 patchset=int(patchset) if patchset else None,
1043 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001044
1045
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001046def _create_description_from_log(args):
1047 """Pulls out the commit log to use as a base for the CL description."""
1048 log_args = []
1049 if len(args) == 1 and not args[0].endswith('.'):
1050 log_args = [args[0] + '..']
1051 elif len(args) == 1 and args[0].endswith('...'):
1052 log_args = [args[0][:-1]]
1053 elif len(args) == 2:
1054 log_args = [args[0] + '..' + args[1]]
1055 else:
1056 log_args = args[:] # Hope for the best!
1057 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1058
1059
Aaron Gablea45ee112016-11-22 15:14:38 -08001060class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001061 def __init__(self, issue, url):
1062 self.issue = issue
1063 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001064 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001065
1066 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001067 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001068 self.issue, self.url)
1069
1070
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001071_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001072 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001073 # TODO(tandrii): these two aren't known in Gerrit.
1074 'approval', 'disapproval'])
1075
1076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001078 """Changelist works with one changelist in local branch.
1079
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001080 Notes:
1081 * Not safe for concurrent multi-{thread,process} use.
1082 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001083 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001084 """
1085
Edward Lemur125d60a2019-09-13 18:25:41 +00001086 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001087 """Create a new ChangeList instance.
1088
Edward Lemurf38bc172019-09-03 21:02:13 +00001089 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001090 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001092 global settings
1093 if not settings:
1094 # Happens when git_cl.py is used as a utility library.
1095 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.branchref = branchref
1098 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001099 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.branch = ShortBranchName(self.branchref)
1101 else:
1102 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001104 self.lookedup_issue = False
1105 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = False
1107 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001108 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001110 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001111 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001112 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001113 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114
Edward Lemur125d60a2019-09-13 18:25:41 +00001115 # Lazily cached values.
1116 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1117 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1118 # Map from change number (issue) to its detail cache.
1119 self._detail_cache = {}
1120
1121 if codereview_host is not None:
1122 assert not codereview_host.startswith('https://'), codereview_host
1123 self._gerrit_host = codereview_host
1124 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001125
1126 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001127 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001128
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001129 The return value is a string suitable for passing to git cl with the --cc
1130 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001131 """
1132 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001133 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001134 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001135 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1136 return self.cc
1137
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001138 def GetCCListWithoutDefault(self):
1139 """Return the users cc'd on this CL excluding default ones."""
1140 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001141 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001142 return self.cc
1143
Daniel Cheng7227d212017-11-17 08:12:37 -08001144 def ExtendCC(self, more_cc):
1145 """Extends the list of users to cc on this CL based on the changed files."""
1146 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147
1148 def GetBranch(self):
1149 """Returns the short branch name, e.g. 'master'."""
1150 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001151 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001152 if not branchref:
1153 return None
1154 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 self.branch = ShortBranchName(self.branchref)
1156 return self.branch
1157
1158 def GetBranchRef(self):
1159 """Returns the full branch name, e.g. 'refs/heads/master'."""
1160 self.GetBranch() # Poke the lazy loader.
1161 return self.branchref
1162
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001163 def ClearBranch(self):
1164 """Clears cached branch data of this object."""
1165 self.branch = self.branchref = None
1166
tandrii5d48c322016-08-18 16:19:37 -07001167 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1168 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1169 kwargs['branch'] = self.GetBranch()
1170 return _git_get_branch_config_value(key, default, **kwargs)
1171
1172 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1173 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1174 assert self.GetBranch(), (
1175 'this CL must have an associated branch to %sset %s%s' %
1176 ('un' if value is None else '',
1177 key,
1178 '' if value is None else ' to %r' % value))
1179 kwargs['branch'] = self.GetBranch()
1180 return _git_set_branch_config_value(key, value, **kwargs)
1181
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001182 @staticmethod
1183 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001184 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 e.g. 'origin', 'refs/heads/master'
1186 """
1187 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001188 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001191 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001193 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1194 error_ok=True).strip()
1195 if upstream_branch:
1196 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001198 # Else, try to guess the origin remote.
1199 remote_branches = RunGit(['branch', '-r']).split()
1200 if 'origin/master' in remote_branches:
1201 # Fall back on origin/master if it exits.
1202 remote = 'origin'
1203 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001205 DieWithError(
1206 'Unable to determine default branch to diff against.\n'
1207 'Either pass complete "git diff"-style arguments, like\n'
1208 ' git cl upload origin/master\n'
1209 'or verify this branch is set up to track another \n'
1210 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 return remote, upstream_branch
1213
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001214 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001215 upstream_branch = self.GetUpstreamBranch()
1216 if not BranchExists(upstream_branch):
1217 DieWithError('The upstream for the current branch (%s) does not exist '
1218 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001219 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001220 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001221
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 def GetUpstreamBranch(self):
1223 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001224 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001225 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001226 upstream_branch = upstream_branch.replace('refs/heads/',
1227 'refs/remotes/%s/' % remote)
1228 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1229 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 self.upstream_branch = upstream_branch
1231 return self.upstream_branch
1232
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001233 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001234 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001235 remote, branch = None, self.GetBranch()
1236 seen_branches = set()
1237 while branch not in seen_branches:
1238 seen_branches.add(branch)
1239 remote, branch = self.FetchUpstreamTuple(branch)
1240 branch = ShortBranchName(branch)
1241 if remote != '.' or branch.startswith('refs/remotes'):
1242 break
1243 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001244 remotes = RunGit(['remote'], error_ok=True).split()
1245 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001247 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001248 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001249 logging.warn('Could not determine which remote this change is '
1250 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001251 else:
1252 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001253 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001254 branch = 'HEAD'
1255 if branch.startswith('refs/remotes'):
1256 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001257 elif branch.startswith('refs/branch-heads/'):
1258 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001259 else:
1260 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001261 return self._remote
1262
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001263 def GitSanityChecks(self, upstream_git_obj):
1264 """Checks git repo status and ensures diff is from local commits."""
1265
sbc@chromium.org79706062015-01-14 21:18:12 +00001266 if upstream_git_obj is None:
1267 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001268 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001269 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001270 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001271 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001272 return False
1273
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001274 # Verify the commit we're diffing against is in our current branch.
1275 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1276 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1277 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001278 print('ERROR: %s is not in the current branch. You may need to rebase '
1279 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001280 return False
1281
1282 # List the commits inside the diff, and verify they are all local.
1283 commits_in_diff = RunGit(
1284 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1285 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1286 remote_branch = remote_branch.strip()
1287 if code != 0:
1288 _, remote_branch = self.GetRemoteBranch()
1289
1290 commits_in_remote = RunGit(
1291 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1292
1293 common_commits = set(commits_in_diff) & set(commits_in_remote)
1294 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001295 print('ERROR: Your diff contains %d commits already in %s.\n'
1296 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1297 'the diff. If you are using a custom git flow, you can override'
1298 ' the reference used for this check with "git config '
1299 'gitcl.remotebranch <git-ref>".' % (
1300 len(common_commits), remote_branch, upstream_git_obj),
1301 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 return False
1303 return True
1304
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001305 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001306 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001307
1308 Returns None if it is not set.
1309 """
tandrii5d48c322016-08-18 16:19:37 -07001310 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001311
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 def GetRemoteUrl(self):
1313 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1314
1315 Returns None if there is no remote.
1316 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001317 is_cached, value = self._cached_remote_url
1318 if is_cached:
1319 return value
1320
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001322 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1323
Edward Lemur298f2cf2019-02-22 21:40:39 +00001324 # Check if the remote url can be parsed as an URL.
Edward Lesmesf6a22322019-11-04 22:14:39 +00001325 host = urlparse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001326 if host:
1327 self._cached_remote_url = (True, url)
1328 return url
1329
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001330 # If it cannot be parsed as an url, assume it is a local directory,
1331 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001332 logging.warning('"%s" doesn\'t appear to point to a git host. '
1333 'Interpreting it as a local directory.', url)
1334 if not os.path.isdir(url):
1335 logging.error(
1336 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
Daniel Bratell4a60db42019-09-16 17:02:52 +00001337 remote, self.GetBranch(), url)
Edward Lemur298f2cf2019-02-22 21:40:39 +00001338 return None
1339
1340 cache_path = url
1341 url = RunGit(['config', 'remote.%s.url' % remote],
1342 error_ok=True,
1343 cwd=url).strip()
1344
Edward Lesmesf6a22322019-11-04 22:14:39 +00001345 host = urlparse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001346 if not host:
1347 logging.error(
1348 'Remote "%(remote)s" for branch "%(branch)s" points to '
1349 '"%(cache_path)s", but it is misconfigured.\n'
1350 '"%(cache_path)s" must be a git repo and must have a remote named '
1351 '"%(remote)s" pointing to the git host.', {
1352 'remote': remote,
1353 'cache_path': cache_path,
1354 'branch': self.GetBranch()})
1355 return None
1356
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001357 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001358 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001360 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001361 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001362 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001363 self.issue = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001364 self.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001365 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 return self.issue
1367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368 def GetIssueURL(self):
1369 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001370 issue = self.GetIssue()
1371 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001372 return None
Edward Lemur125d60a2019-09-13 18:25:41 +00001373 return '%s/%s' % (self.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001375 def GetDescription(self, pretty=False, force=False):
1376 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 if self.GetIssue():
Edward Lemur125d60a2019-09-13 18:25:41 +00001378 self.description = self.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 self.has_description = True
1380 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001381 # Set width to 72 columns + 2 space indent.
1382 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001384 lines = self.description.splitlines()
1385 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386 return self.description
1387
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001388 def GetDescriptionFooters(self):
1389 """Returns (non_footer_lines, footers) for the commit message.
1390
1391 Returns:
1392 non_footer_lines (list(str)) - Simple list of description lines without
1393 any footer. The lines do not contain newlines, nor does the list contain
1394 the empty line between the message and the footers.
1395 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1396 [("Change-Id", "Ideadbeef...."), ...]
1397 """
1398 raw_description = self.GetDescription()
1399 msg_lines, _, footers = git_footers.split_footers(raw_description)
1400 if footers:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001401 msg_lines = msg_lines[:len(msg_lines) - 1]
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001402 return msg_lines, footers
1403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001405 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001406 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001407 self.patchset = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001408 self.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 return self.patchset
1411
1412 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001413 """Set this branch's patchset. If patchset=0, clears the patchset."""
1414 assert self.GetBranch()
1415 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001416 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001417 else:
1418 self.patchset = int(patchset)
1419 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001420 self.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001422 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001423 """Set this branch's issue. If issue isn't given, clears the issue."""
1424 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001426 issue = int(issue)
1427 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001428 self.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001429 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001430 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001431 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001432 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001433 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001434 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 else:
tandrii5d48c322016-08-18 16:19:37 -07001436 # Reset all of these just to be clean.
1437 reset_suffixes = [
1438 'last-upload-hash',
Edward Lemur125d60a2019-09-13 18:25:41 +00001439 self.IssueConfigKey(),
1440 self.PatchsetConfigKey(),
1441 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001442 ] + self._PostUnsetIssueProperties()
1443 for prop in reset_suffixes:
1444 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001445 msg = RunGit(['log', '-1', '--format=%B']).strip()
1446 if msg and git_footers.get_footer_change_id(msg):
1447 print('WARNING: The change patched into this branch has a Change-Id. '
1448 'Removing it.')
1449 RunGit(['commit', '--amend', '-m',
1450 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001451 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001452 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001453 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
dnjba1b0f32016-09-02 12:37:42 -07001455 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001456 if not self.GitSanityChecks(upstream_branch):
1457 DieWithError('\nGit sanity check failure')
1458
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001459 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001460 if not root:
1461 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001462 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001463
1464 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001465 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001466 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001468 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 except subprocess2.CalledProcessError:
1470 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001471 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001472 'This branch probably doesn\'t exist anymore. To reset the\n'
1473 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001474 ' git branch --set-upstream-to origin/master %s\n'
1475 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001476 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001477
maruel@chromium.org52424302012-08-29 15:14:30 +00001478 issue = self.GetIssue()
1479 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001480 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001481 description = self.GetDescription()
1482 else:
1483 # If the change was never uploaded, use the log messages of all commits
1484 # up to the branch point, as git cl upload will prefill the description
1485 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1487 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001488
1489 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001490 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001491 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001492 name,
1493 description,
1494 absroot,
1495 files,
1496 issue,
1497 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001498 author,
1499 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001500
dsansomee2d6fd92016-09-08 00:10:47 -07001501 def UpdateDescription(self, description, force=False):
Edward Lemur125d60a2019-09-13 18:25:41 +00001502 self.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001503 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001504 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001505
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001506 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1507 """Sets the description for this CL remotely.
1508
1509 You can get description_lines and footers with GetDescriptionFooters.
1510
1511 Args:
1512 description_lines (list(str)) - List of CL description lines without
1513 newline characters.
1514 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1515 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1516 `List-Of-Tokens`). It will be case-normalized so that each token is
1517 title-cased.
1518 """
1519 new_description = '\n'.join(description_lines)
1520 if footers:
1521 new_description += '\n'
1522 for k, v in footers:
1523 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1524 if not git_footers.FOOTER_PATTERN.match(foot):
1525 raise ValueError('Invalid footer %r' % foot)
1526 new_description += foot + '\n'
1527 self.UpdateDescription(new_description, force)
1528
Edward Lesmes8e282792018-04-03 18:50:29 -04001529 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1531 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001532 start = time_time()
1533 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1535 default_presubmit=None, may_prompt=may_prompt,
Edward Lemur125d60a2019-09-13 18:25:41 +00001536 gerrit_obj=self.GetGerritObjForPresubmit(),
Edward Lesmes8e282792018-04-03 18:50:29 -04001537 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001538 metrics.collector.add_repeated('sub_commands', {
1539 'command': 'presubmit',
1540 'execution_time': time_time() - start,
1541 'exit_code': 0 if result.should_continue() else 1,
1542 })
1543 return result
vapierfd77ac72016-06-16 08:33:57 -07001544 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001545 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001546
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 def CMDUpload(self, options, git_diff_args, orig_args):
1548 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001549 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001551 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001552 else:
1553 if self.GetBranch() is None:
1554 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1555
1556 # Default to diffing against common ancestor of upstream branch
1557 base_branch = self.GetCommonAncestorWithUpstream()
1558 git_diff_args = [base_branch, 'HEAD']
1559
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001560 # Fast best-effort checks to abort before running potentially expensive
1561 # hooks if uploading is likely to fail anyway. Passing these checks does
1562 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001563 self.EnsureAuthenticated(force=options.force)
1564 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001565
1566 # Apply watchlists on upload.
1567 change = self.GetChange(base_branch, None)
1568 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1569 files = [f.LocalPath() for f in change.AffectedFiles()]
1570 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001571 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001572
1573 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001574 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 # Set the reviewer list now so that presubmit checks can access it.
1576 change_description = ChangeDescription(change.FullDescriptionText())
1577 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001578 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001579 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 change)
1581 change.SetDescriptionText(change_description.description)
1582 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001583 may_prompt=not options.force,
1584 verbose=options.verbose,
1585 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 if not hook_results.should_continue():
1587 return 1
1588 if not options.reviewers and hook_results.reviewers:
1589 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001590 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001591
Aaron Gable13101a62018-02-09 13:20:41 -08001592 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001593 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001595 _git_set_branch_config_value('last-upload-hash',
1596 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001597 # Run post upload hooks, if specified.
1598 if settings.GetRunPostUploadHook():
1599 presubmit_support.DoPostUploadExecuter(
1600 change,
1601 self,
1602 settings.GetRoot(),
1603 options.verbose,
1604 sys.stdout)
1605
1606 # Upload all dependencies if specified.
1607 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001608 print()
1609 print('--dependencies has been specified.')
1610 print('All dependent local branches will be re-uploaded.')
1611 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001612 # Remove the dependencies flag from args so that we do not end up in a
1613 # loop.
1614 orig_args.remove('--dependencies')
1615 ret = upload_branch_deps(self, orig_args)
1616 return ret
1617
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001618 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001619 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001620
1621 Issue must have been already uploaded and known.
1622 """
1623 assert new_state in _CQState.ALL_STATES
1624 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001625 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001626 vote_map = {
1627 _CQState.NONE: 0,
1628 _CQState.DRY_RUN: 1,
1629 _CQState.COMMIT: 2,
1630 }
1631 labels = {'Commit-Queue': vote_map[new_state]}
1632 notify = False if new_state == _CQState.DRY_RUN else None
1633 gerrit_util.SetReview(
1634 self._GetGerritHost(), self._GerritChangeIdentifier(),
1635 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001636 return 0
1637 except KeyboardInterrupt:
1638 raise
1639 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001640 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001641 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001642 ' * Your project has no CQ,\n'
1643 ' * You don\'t have permission to change the CQ state,\n'
1644 ' * There\'s a bug in this code (see stack trace below).\n'
1645 'Consider specifying which bots to trigger manually or asking your '
1646 'project owners for permissions or contacting Chrome Infra at:\n'
1647 'https://www.chromium.org/infra\n\n' %
1648 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001649 # Still raise exception so that stack trace is printed.
1650 raise
1651
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001652 def _GetGerritHost(self):
1653 # Lazy load of configs.
1654 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001655 if self._gerrit_host and '.' not in self._gerrit_host:
1656 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1657 # This happens for internal stuff http://crbug.com/614312.
Edward Lesmesf6a22322019-11-04 22:14:39 +00001658 parsed = urlparse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001659 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001660 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001661 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001662 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1663 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001664 return self._gerrit_host
1665
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001666 def _GetGitHost(self):
1667 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001668 remote_url = self.GetRemoteUrl()
1669 if not remote_url:
1670 return None
Edward Lesmesf6a22322019-11-04 22:14:39 +00001671 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001672
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001673 def GetCodereviewServer(self):
1674 if not self._gerrit_server:
1675 # If we're on a branch then get the server potentially associated
1676 # with that branch.
1677 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001678 self._gerrit_server = self._GitGetBranchConfigValue(
1679 self.CodereviewServerConfigKey())
1680 if self._gerrit_server:
Edward Lesmesf6a22322019-11-04 22:14:39 +00001681 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 if not self._gerrit_server:
1683 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1684 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001685 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001686 parts[0] = parts[0] + '-review'
1687 self._gerrit_host = '.'.join(parts)
1688 self._gerrit_server = 'https://%s' % self._gerrit_host
1689 return self._gerrit_server
1690
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001691 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001692 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001693 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001694 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001695 logging.warn('can\'t detect Gerrit project.')
1696 return None
Edward Lesmesf6a22322019-11-04 22:14:39 +00001697 project = urlparse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001698 if project.endswith('.git'):
1699 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001700 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1701 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1702 # gitiles/git-over-https protocol. E.g.,
1703 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1704 # as
1705 # https://chromium.googlesource.com/v8/v8
1706 if project.startswith('a/'):
1707 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001708 return project
1709
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001710 def _GerritChangeIdentifier(self):
1711 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1712
1713 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001714 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001715 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001716 project = self._GetGerritProject()
1717 if project:
1718 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1719 # Fall back on still unique, but less efficient change number.
1720 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001721
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001722 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001723 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001724 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725
tandrii5d48c322016-08-18 16:19:37 -07001726 @classmethod
1727 def PatchsetConfigKey(cls):
1728 return 'gerritpatchset'
1729
1730 @classmethod
1731 def CodereviewServerConfigKey(cls):
1732 return 'gerritserver'
1733
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001734 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001735 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001736 if settings.GetGerritSkipEnsureAuthenticated():
1737 # For projects with unusual authentication schemes.
1738 # See http://crbug.com/603378.
1739 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001740
1741 # Check presence of cookies only if using cookies-based auth method.
1742 cookie_auth = gerrit_util.Authenticator.get()
1743 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001744 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001745
Edward Lesmesf6a22322019-11-04 22:14:39 +00001746 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
Daniel Chengcf6269b2019-05-18 01:02:12 +00001747 print('WARNING: Ignoring branch %s with non-https remote %s' %
Edward Lemur125d60a2019-09-13 18:25:41 +00001748 (self.branch, self.GetRemoteUrl()))
Daniel Chengcf6269b2019-05-18 01:02:12 +00001749 return
1750
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001751 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001752 self.GetCodereviewServer()
1753 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001754 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001755
1756 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1757 git_auth = cookie_auth.get_auth_header(git_host)
1758 if gerrit_auth and git_auth:
1759 if gerrit_auth == git_auth:
1760 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001761 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001762 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001763 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001764 ' %s\n'
1765 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001766 ' Consider running the following command:\n'
1767 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001768 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001769 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001770 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001771 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001772 cookie_auth.get_new_password_message(git_host)))
1773 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001774 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001775 return
1776 else:
1777 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001778 ([] if gerrit_auth else [self._gerrit_host]) +
1779 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001780 DieWithError('Credentials for the following hosts are required:\n'
1781 ' %s\n'
1782 'These are read from %s (or legacy %s)\n'
1783 '%s' % (
1784 '\n '.join(missing),
1785 cookie_auth.get_gitcookies_path(),
1786 cookie_auth.get_netrc_path(),
1787 cookie_auth.get_new_password_message(git_host)))
1788
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001789 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001790 if not self.GetIssue():
1791 return
1792
1793 # Warm change details cache now to avoid RPCs later, reducing latency for
1794 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001795 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001796 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001797
1798 status = self._GetChangeDetail()['status']
1799 if status in ('MERGED', 'ABANDONED'):
1800 DieWithError('Change %s has been %s, new uploads are not allowed' %
1801 (self.GetIssueURL(),
1802 'submitted' if status == 'MERGED' else 'abandoned'))
1803
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001804 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1805 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1806 # Apparently this check is not very important? Otherwise get_auth_email
1807 # could have been added to other implementations of Authenticator.
1808 cookies_auth = gerrit_util.Authenticator.get()
1809 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001810 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001811
1812 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001813 if self.GetIssueOwner() == cookies_user:
1814 return
1815 logging.debug('change %s owner is %s, cookies user is %s',
1816 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001817 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001818 # so ask what Gerrit thinks of this user.
1819 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1820 if details['email'] == self.GetIssueOwner():
1821 return
1822 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001823 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001824 'as %s.\n'
1825 'Uploading may fail due to lack of permissions.' %
1826 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1827 confirm_or_exit(action='upload')
1828
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001829 def _PostUnsetIssueProperties(self):
1830 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001831 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001832
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001833 def GetGerritObjForPresubmit(self):
1834 return presubmit_support.GerritAccessor(self._GetGerritHost())
1835
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001837 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001838 or CQ status, assuming adherence to a common workflow.
1839
1840 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001841 * 'error' - error from review tool (including deleted issues)
1842 * 'unsent' - no reviewers added
1843 * 'waiting' - waiting for review
1844 * 'reply' - waiting for uploader to reply to review
1845 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001846 * 'dry-run' - dry-running in the CQ
1847 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001848 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001849 """
1850 if not self.GetIssue():
1851 return None
1852
1853 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001854 data = self._GetChangeDetail([
1855 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lesmesf6a22322019-11-04 22:14:39 +00001856 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001857 return 'error'
1858
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001859 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001860 return 'closed'
1861
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001862 cq_label = data['labels'].get('Commit-Queue', {})
1863 max_cq_vote = 0
1864 for vote in cq_label.get('all', []):
1865 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1866 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001867 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001868 if max_cq_vote == 1:
1869 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001870
Aaron Gable9ab38c62017-04-06 14:36:33 -07001871 if data['labels'].get('Code-Review', {}).get('approved'):
1872 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001873
1874 if not data.get('reviewers', {}).get('REVIEWER', []):
1875 return 'unsent'
1876
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001877 owner = data['owner'].get('_account_id')
Edward Lesmesf6a22322019-11-04 22:14:39 +00001878 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
Aaron Gable9ab38c62017-04-06 14:36:33 -07001879 last_message_author = messages.pop().get('author', {})
1880 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001881 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
1882 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07001883 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001884 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07001885 if last_message_author.get('_account_id') == owner:
1886 # Most recent message was by owner.
1887 return 'waiting'
1888 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001889 # Some reply from non-owner.
1890 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001891
1892 # Somehow there are no messages even though there are reviewers.
1893 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001894
1895 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001896 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001897 patchset = data['revisions'][data['current_revision']]['_number']
1898 self.SetPatchset(patchset)
1899 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001900
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001901 def FetchDescription(self, force=False):
1902 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
1903 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00001904 current_rev = data['current_revision']
Edward Lesmesf6a22322019-11-04 22:14:39 +00001905 return data['revisions'][current_rev]['commit']['message'].encode(
1906 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001907
dsansomee2d6fd92016-09-08 00:10:47 -07001908 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001909 if gerrit_util.HasPendingChangeEdit(
1910 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07001911 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001912 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07001913 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001914 'unpublished edit. Either publish the edit in the Gerrit web UI '
1915 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07001916
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001917 gerrit_util.DeletePendingChangeEdit(
1918 self._GetGerritHost(), self._GerritChangeIdentifier())
1919 gerrit_util.SetCommitMessage(
1920 self._GetGerritHost(), self._GerritChangeIdentifier(),
1921 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922
Aaron Gable636b13f2017-07-14 10:42:48 -07001923 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001924 gerrit_util.SetReview(
1925 self._GetGerritHost(), self._GerritChangeIdentifier(),
1926 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001927
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001928 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001929 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001930 # CURRENT_REVISION is included to get the latest patchset so that
1931 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001932 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001933 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1934 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001935 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001936 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001937 robot_file_comments = gerrit_util.GetChangeRobotComments(
1938 self._GetGerritHost(), self._GerritChangeIdentifier())
1939
1940 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00001941 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001942 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001943 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001944 line_comments = file_comments.setdefault(path, [])
1945 line_comments.extend(
1946 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001947
1948 # Build dictionary of file comments for easy access and sorting later.
1949 # {author+date: {path: {patchset: {line: url+message}}}}
1950 comments = collections.defaultdict(
1951 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00001952 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001953 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001954 tag = comment.get('tag', '')
1955 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001956 continue
1957 key = (comment['author']['email'], comment['updated'])
1958 if comment.get('side', 'REVISION') == 'PARENT':
1959 patchset = 'Base'
1960 else:
1961 patchset = 'PS%d' % comment['patch_set']
1962 line = comment.get('line', 0)
1963 url = ('https://%s/c/%s/%s/%s#%s%s' %
1964 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1965 'b' if comment.get('side') == 'PARENT' else '',
1966 str(line) if line else ''))
1967 comments[key][path][patchset][line] = (url, comment['message'])
1968
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001969 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001970 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001971 summary = self._BuildCommentSummary(msg, comments, readable)
1972 if summary:
1973 summaries.append(summary)
1974 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001975
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001976 @staticmethod
1977 def _BuildCommentSummary(msg, comments, readable):
1978 key = (msg['author']['email'], msg['date'])
1979 # Don't bother showing autogenerated messages that don't have associated
1980 # file or line comments. this will filter out most autogenerated
1981 # messages, but will keep robot comments like those from Tricium.
1982 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1983 if is_autogenerated and not comments.get(key):
1984 return None
1985 message = msg['message']
1986 # Gerrit spits out nanoseconds.
1987 assert len(msg['date'].split('.')[-1]) == 9
1988 date = datetime.datetime.strptime(msg['date'][:-3],
1989 '%Y-%m-%d %H:%M:%S.%f')
1990 if key in comments:
1991 message += '\n'
1992 for path, patchsets in sorted(comments.get(key, {}).items()):
1993 if readable:
1994 message += '\n%s' % path
1995 for patchset, lines in sorted(patchsets.items()):
1996 for line, (url, content) in sorted(lines.items()):
1997 if line:
1998 line_str = 'Line %d' % line
1999 path_str = '%s:%d:' % (path, line)
2000 else:
2001 line_str = 'File comment'
2002 path_str = '%s:0:' % path
2003 if readable:
2004 message += '\n %s, %s: %s' % (patchset, line_str, url)
2005 message += '\n %s\n' % content
2006 else:
2007 message += '\n%s ' % path_str
2008 message += '\n%s\n' % content
2009
2010 return _CommentSummary(
2011 date=date,
2012 message=message,
2013 sender=msg['author']['email'],
2014 autogenerated=is_autogenerated,
2015 # These could be inferred from the text messages and correlated with
2016 # Code-Review label maximum, however this is not reliable.
2017 # Leaving as is until the need arises.
2018 approval=False,
2019 disapproval=False,
2020 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002021
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002022 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002023 gerrit_util.AbandonChange(
2024 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002025
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002026 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002027 gerrit_util.SubmitChange(
2028 self._GetGerritHost(), self._GerritChangeIdentifier(),
2029 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002031 def _GetChangeDetail(self, options=None, no_cache=False):
2032 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002033
2034 If fresh data is needed, set no_cache=True which will clear cache and
2035 thus new data will be fetched from Gerrit.
2036 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002037 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002038 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002039
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002040 # Optimization to avoid multiple RPCs:
2041 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2042 'CURRENT_COMMIT' not in options):
2043 options.append('CURRENT_COMMIT')
2044
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002045 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002046 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002047 options = [o.upper() for o in options]
2048
2049 # Check in cache first unless no_cache is True.
2050 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002051 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002052 else:
2053 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002054 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002055 # Assumption: data fetched before with extra options is suitable
2056 # for return for a smaller set of options.
2057 # For example, if we cached data for
2058 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2059 # and request is for options=[CURRENT_REVISION],
2060 # THEN we can return prior cached data.
2061 if options_set.issubset(cached_options_set):
2062 return data
2063
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002064 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002065 data = gerrit_util.GetChangeDetail(
2066 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002067 except gerrit_util.GerritError as e:
2068 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002069 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002070 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002071
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002072 self._detail_cache.setdefault(cache_key, []).append(
2073 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002074 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002075
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002076 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002077 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002078 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002079 data = gerrit_util.GetChangeCommit(
2080 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002081 except gerrit_util.GerritError as e:
2082 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002083 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002084 raise
agable32978d92016-11-01 12:55:02 -07002085 return data
2086
Karen Qian40c19422019-03-13 21:28:29 +00002087 def _IsCqConfigured(self):
2088 detail = self._GetChangeDetail(['LABELS'])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002089 if u'Commit-Queue' not in detail.get('labels', {}):
Karen Qian40c19422019-03-13 21:28:29 +00002090 return False
2091 # TODO(crbug/753213): Remove temporary hack
2092 if ('https://chromium.googlesource.com/chromium/src' ==
Edward Lemur125d60a2019-09-13 18:25:41 +00002093 self.GetRemoteUrl() and
Karen Qian40c19422019-03-13 21:28:29 +00002094 detail['branch'].startswith('refs/branch-heads/')):
2095 return False
2096 return True
2097
Olivier Robin75ee7252018-04-13 10:02:56 +02002098 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002099 if git_common.is_dirty_git_tree('land'):
2100 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002101
tandriid60367b2016-06-22 05:25:12 -07002102 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002103 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002104 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002105 'which can test and land changes for you. '
2106 'Are you sure you wish to bypass it?\n',
2107 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002108 differs = True
tandriic4344b52016-08-29 06:04:54 -07002109 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002110 # Note: git diff outputs nothing if there is no diff.
2111 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002112 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002113 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002114 if detail['current_revision'] == last_upload:
2115 differs = False
2116 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002117 print('WARNING: Local branch contents differ from latest uploaded '
2118 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002119 if differs:
2120 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002121 confirm_or_exit(
2122 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2123 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002124 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002125 elif not bypass_hooks:
2126 hook_results = self.RunHook(
2127 committing=True,
2128 may_prompt=not force,
2129 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002130 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2131 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002132 if not hook_results.should_continue():
2133 return 1
2134
2135 self.SubmitIssue(wait_for_merge=True)
2136 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002137 links = self._GetChangeCommit().get('web_links', [])
2138 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002139 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002140 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002141 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002142 return 0
2143
Edward Lemurf38bc172019-09-03 21:02:13 +00002144 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 assert parsed_issue_arg.valid
2146
Edward Lemur125d60a2019-09-13 18:25:41 +00002147 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002148
2149 if parsed_issue_arg.hostname:
2150 self._gerrit_host = parsed_issue_arg.hostname
2151 self._gerrit_server = 'https://%s' % self._gerrit_host
2152
tandriic2405f52016-10-10 08:13:15 -07002153 try:
2154 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002155 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002156 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157
2158 if not parsed_issue_arg.patchset:
2159 # Use current revision by default.
2160 revision_info = detail['revisions'][detail['current_revision']]
2161 patchset = int(revision_info['_number'])
2162 else:
2163 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002164 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2166 break
2167 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002168 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002169 (parsed_issue_arg.patchset, self.GetIssue()))
2170
Edward Lemur125d60a2019-09-13 18:25:41 +00002171 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002172 if remote_url.endswith('.git'):
2173 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002174 remote_url = remote_url.rstrip('/')
2175
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002176 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002177 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002178
2179 if remote_url != fetch_info['url']:
2180 DieWithError('Trying to patch a change from %s but this repo appears '
2181 'to be %s.' % (fetch_info['url'], remote_url))
2182
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002183 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002184
Aaron Gable62619a32017-06-16 08:22:09 -07002185 if force:
2186 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2187 print('Checked out commit for change %i patchset %i locally' %
2188 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002189 elif nocommit:
2190 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2191 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002192 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002193 RunGit(['cherry-pick', 'FETCH_HEAD'])
2194 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002195 (parsed_issue_arg.issue, patchset))
2196 print('Note: this created a local commit which does not have '
2197 'the same hash as the one uploaded for review. This will make '
2198 'uploading changes based on top of this branch difficult.\n'
2199 'If you want to do that, use "git cl patch --force" instead.')
2200
Stefan Zagerd08043c2017-10-12 12:07:02 -07002201 if self.GetBranch():
2202 self.SetIssue(parsed_issue_arg.issue)
2203 self.SetPatchset(patchset)
2204 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2205 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2206 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2207 else:
2208 print('WARNING: You are in detached HEAD state.\n'
2209 'The patch has been applied to your checkout, but you will not be '
2210 'able to upload a new patch set to the gerrit issue.\n'
2211 'Try using the \'-b\' option if you would like to work on a '
2212 'branch and/or upload a new patch set.')
2213
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002214 return 0
2215
tandrii16e0b4e2016-06-07 10:34:28 -07002216 def _GerritCommitMsgHookCheck(self, offer_removal):
2217 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2218 if not os.path.exists(hook):
2219 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002220 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2221 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002222 data = gclient_utils.FileRead(hook)
2223 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2224 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002225 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002226 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002227 'and may interfere with it in subtle ways.\n'
2228 'We recommend you remove the commit-msg hook.')
2229 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002230 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002231 gclient_utils.rm_file_or_tree(hook)
2232 print('Gerrit commit-msg hook removed.')
2233 else:
2234 print('OK, will keep Gerrit commit-msg hook in place.')
2235
Edward Lemur1b52d872019-05-09 21:12:12 +00002236 def _CleanUpOldTraces(self):
2237 """Keep only the last |MAX_TRACES| traces."""
2238 try:
2239 traces = sorted([
2240 os.path.join(TRACES_DIR, f)
2241 for f in os.listdir(TRACES_DIR)
2242 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2243 and not f.startswith('tmp'))
2244 ])
2245 traces_to_delete = traces[:-MAX_TRACES]
2246 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002247 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002248 except OSError:
2249 print('WARNING: Failed to remove old git traces from\n'
2250 ' %s'
2251 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002252
Edward Lemur5737f022019-05-17 01:24:00 +00002253 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002254 """Zip and write the git push traces stored in traces_dir."""
2255 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002256 traces_zip = trace_name + '-traces'
2257 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002258 # Create a temporary dir to store git config and gitcookies in. It will be
2259 # compressed and stored next to the traces.
2260 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002261 git_info_zip = trace_name + '-git-info'
2262
Edward Lemur5737f022019-05-17 01:24:00 +00002263 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002264 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002265 git_push_metadata['now'] = git_push_metadata['now'].decode(
2266 sys.stdin.encoding)
2267
Edward Lemur1b52d872019-05-09 21:12:12 +00002268 git_push_metadata['trace_name'] = trace_name
2269 gclient_utils.FileWrite(
2270 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2271
2272 # Keep only the first 6 characters of the git hashes on the packet
2273 # trace. This greatly decreases size after compression.
2274 packet_traces = os.path.join(traces_dir, 'trace-packet')
2275 if os.path.isfile(packet_traces):
2276 contents = gclient_utils.FileRead(packet_traces)
2277 gclient_utils.FileWrite(
2278 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2279 shutil.make_archive(traces_zip, 'zip', traces_dir)
2280
2281 # Collect and compress the git config and gitcookies.
2282 git_config = RunGit(['config', '-l'])
2283 gclient_utils.FileWrite(
2284 os.path.join(git_info_dir, 'git-config'),
2285 git_config)
2286
2287 cookie_auth = gerrit_util.Authenticator.get()
2288 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2289 gitcookies_path = cookie_auth.get_gitcookies_path()
2290 if os.path.isfile(gitcookies_path):
2291 gitcookies = gclient_utils.FileRead(gitcookies_path)
2292 gclient_utils.FileWrite(
2293 os.path.join(git_info_dir, 'gitcookies'),
2294 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2295 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2296
Edward Lemur1b52d872019-05-09 21:12:12 +00002297 gclient_utils.rmtree(git_info_dir)
2298
2299 def _RunGitPushWithTraces(
2300 self, change_desc, refspec, refspec_opts, git_push_metadata):
2301 """Run git push and collect the traces resulting from the execution."""
2302 # Create a temporary directory to store traces in. Traces will be compressed
2303 # and stored in a 'traces' dir inside depot_tools.
2304 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002305 trace_name = os.path.join(
2306 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002307
2308 env = os.environ.copy()
2309 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2310 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002311 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002312 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2313 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2314 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2315
2316 try:
2317 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002318 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002319 before_push = time_time()
2320 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002321 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002322 env=env,
2323 print_stdout=True,
2324 # Flush after every line: useful for seeing progress when running as
2325 # recipe.
2326 filter_fn=lambda _: sys.stdout.flush())
2327 except subprocess2.CalledProcessError as e:
2328 push_returncode = e.returncode
2329 DieWithError('Failed to create a change. Please examine output above '
2330 'for the reason of the failure.\n'
2331 'Hint: run command below to diagnose common Git/Gerrit '
2332 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002333 ' git cl creds-check\n'
2334 '\n'
2335 'If git-cl is not working correctly, file a bug under the '
2336 'Infra>SDK component including the files below.\n'
2337 'Review the files before upload, since they might contain '
2338 'sensitive information.\n'
2339 'Set the Restrict-View-Google label so that they are not '
2340 'publicly accessible.\n'
2341 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002342 change_desc)
2343 finally:
2344 execution_time = time_time() - before_push
2345 metrics.collector.add_repeated('sub_commands', {
2346 'command': 'git push',
2347 'execution_time': execution_time,
2348 'exit_code': push_returncode,
2349 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2350 })
2351
Edward Lemur1b52d872019-05-09 21:12:12 +00002352 git_push_metadata['execution_time'] = execution_time
2353 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002354 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002355
Edward Lemur1b52d872019-05-09 21:12:12 +00002356 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002357 gclient_utils.rmtree(traces_dir)
2358
2359 return push_stdout
2360
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002361 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002362 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002363 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002364 # Load default for user, repo, squash=true, in this order.
2365 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002366
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002367 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002368 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002369 # This may be None; default fallback value is determined in logic below.
2370 title = options.title
2371
Dominic Battre7d1c4842017-10-27 09:17:28 +02002372 # Extract bug number from branch name.
2373 bug = options.bug
Dan Beamd8b04ca2019-10-10 21:23:26 +00002374 fixed = options.fixed
2375 match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
2376 self.GetBranch())
2377 if not bug and not fixed and match:
2378 if match.group('type') == 'bug':
2379 bug = match.group('bugnum')
2380 else:
2381 fixed = match.group('bugnum')
Dominic Battre7d1c4842017-10-27 09:17:28 +02002382
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002383 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002384 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002385 if self.GetIssue():
2386 # Try to get the message from a previous upload.
2387 message = self.GetDescription()
2388 if not message:
2389 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002390 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002391 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002392 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002393 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002394 # When uploading a subsequent patchset, -m|--message is taken
2395 # as the patchset title if --title was not provided.
2396 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002397 else:
2398 default_title = RunGit(
2399 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002400 if options.force:
2401 title = default_title
2402 else:
2403 title = ask_for_data(
2404 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002405 change_id = self._GetChangeDetail()['change_id']
2406 while True:
2407 footer_change_ids = git_footers.get_footer_change_id(message)
2408 if footer_change_ids == [change_id]:
2409 break
2410 if not footer_change_ids:
2411 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002412 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002413 continue
2414 # There is already a valid footer but with different or several ids.
2415 # Doing this automatically is non-trivial as we don't want to lose
2416 # existing other footers, yet we want to append just 1 desired
2417 # Change-Id. Thus, just create a new footer, but let user verify the
2418 # new description.
2419 message = '%s\n\nChange-Id: %s' % (message, change_id)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002420 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002421 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002422 print(
2423 'WARNING: change %s has Change-Id footer(s):\n'
2424 ' %s\n'
2425 'but change has Change-Id %s, according to Gerrit.\n'
2426 'Please, check the proposed correction to the description, '
2427 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2428 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2429 change_id))
2430 confirm_or_exit(action='edit')
2431 change_desc.prompt()
2432
2433 message = change_desc.description
2434 if not message:
2435 DieWithError("Description is empty. Aborting...")
2436
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002437 # Continue the while loop.
2438 # Sanity check of this code - we should end up with proper message
2439 # footer.
2440 assert [change_id] == git_footers.get_footer_change_id(message)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002441 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
Aaron Gableb56ad332017-01-06 15:24:31 -08002442 else: # if not self.GetIssue()
2443 if options.message:
2444 message = options.message
2445 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002446 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002447 if options.title:
2448 message = options.title + '\n\n' + message
Dan Beamd8b04ca2019-10-10 21:23:26 +00002449 change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002450 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002451 change_desc.prompt()
2452
Aaron Gableb56ad332017-01-06 15:24:31 -08002453 # On first upload, patchset title is always this string, while
2454 # --title flag gets converted to first line of message.
2455 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002456 if not change_desc.description:
2457 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002458 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002459 if len(change_ids) > 1:
2460 DieWithError('too many Change-Id footers, at most 1 allowed.')
2461 if not change_ids:
2462 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002463 change_desc.set_description(git_footers.add_footer_change_id(
2464 change_desc.description,
2465 GenerateGerritChangeId(change_desc.description)))
2466 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002467 assert len(change_ids) == 1
2468 change_id = change_ids[0]
2469
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002470 if options.reviewers or options.tbrs or options.add_owners_to:
2471 change_desc.update_reviewers(options.reviewers, options.tbrs,
2472 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002473 if options.preserve_tryjobs:
2474 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002475
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002476 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002477 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2478 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002479 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lesmesf6a22322019-11-04 22:14:39 +00002480 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
Aaron Gable9a03ae02017-11-03 11:31:07 -07002481 desc_tempfile.write(change_desc.description)
2482 desc_tempfile.close()
2483 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2484 '-F', desc_tempfile.name]).strip()
2485 os.remove(desc_tempfile.name)
Anthony Polito8b955342019-09-24 19:01:36 +00002486 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002487 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002488 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002489 if not change_desc.description:
2490 DieWithError("Description is empty. Aborting...")
2491
2492 if not git_footers.get_footer_change_id(change_desc.description):
2493 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002494 change_desc.set_description(
2495 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002496 if options.reviewers or options.tbrs or options.add_owners_to:
2497 change_desc.update_reviewers(options.reviewers, options.tbrs,
2498 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002500 # For no-squash mode, we assume the remote called "origin" is the one we
2501 # want. It is not worthwhile to support different workflows for
2502 # no-squash mode.
2503 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002504 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2505
2506 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002507 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002508 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2509 ref_to_push)]).splitlines()
2510 if len(commits) > 1:
2511 print('WARNING: This will upload %d commits. Run the following command '
2512 'to see which commits will be uploaded: ' % len(commits))
2513 print('git log %s..%s' % (parent, ref_to_push))
2514 print('You can also use `git squash-branch` to squash these into a '
2515 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002516 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002517
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002518 if options.reviewers or options.tbrs or options.add_owners_to:
2519 change_desc.update_reviewers(options.reviewers, options.tbrs,
2520 options.add_owners_to, change)
2521
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002522 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002523 cc = []
2524 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2525 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2526 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002527 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002528 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002529 if options.cc:
2530 cc.extend(options.cc)
Edward Lesmesf6a22322019-11-04 22:14:39 +00002531 cc = filter(None, [email.strip() for email in cc])
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002532 if change_desc.get_cced():
2533 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002534 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2535 valid_accounts = set(reviewers + cc)
2536 # TODO(crbug/877717): relax this for all hosts.
2537 else:
2538 valid_accounts = gerrit_util.ValidAccounts(
2539 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002540 logging.info('accounts %s are recognized, %s invalid',
2541 sorted(valid_accounts),
2542 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002543
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002544 # Extra options that can be specified at push time. Doc:
2545 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002546 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002547
Aaron Gable844cf292017-06-28 11:32:59 -07002548 # By default, new changes are started in WIP mode, and subsequent patchsets
2549 # don't send email. At any time, passing --send-mail will mark the change
2550 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002551 if options.send_mail:
2552 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002553 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002554 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002555 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002556 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002557 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002558
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002559 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002560 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002561
Aaron Gable9b713dd2016-12-14 16:04:21 -08002562 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002563 # Punctuation and whitespace in |title| must be percent-encoded.
2564 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002565
agablec6787972016-09-09 16:13:34 -07002566 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002567 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002568
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002569 for r in sorted(reviewers):
2570 if r in valid_accounts:
2571 refspec_opts.append('r=%s' % r)
2572 reviewers.remove(r)
2573 else:
2574 # TODO(tandrii): this should probably be a hard failure.
2575 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2576 % r)
2577 for c in sorted(cc):
2578 # refspec option will be rejected if cc doesn't correspond to an
2579 # account, even though REST call to add such arbitrary cc may succeed.
2580 if c in valid_accounts:
2581 refspec_opts.append('cc=%s' % c)
2582 cc.remove(c)
2583
rmistry9eadede2016-09-19 11:22:43 -07002584 if options.topic:
2585 # Documentation on Gerrit topics is here:
2586 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002587 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002588
Edward Lemur687ca902018-12-05 02:30:30 +00002589 if options.enable_auto_submit:
2590 refspec_opts.append('l=Auto-Submit+1')
2591 if options.use_commit_queue:
2592 refspec_opts.append('l=Commit-Queue+2')
2593 elif options.cq_dry_run:
2594 refspec_opts.append('l=Commit-Queue+1')
2595
2596 if change_desc.get_reviewers(tbr_only=True):
2597 score = gerrit_util.GetCodeReviewTbrScore(
2598 self._GetGerritHost(),
2599 self._GetGerritProject())
2600 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002601
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002602 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002603 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002604 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002605 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002606 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2607
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002608 refspec_suffix = ''
2609 if refspec_opts:
2610 refspec_suffix = '%' + ','.join(refspec_opts)
2611 assert ' ' not in refspec_suffix, (
2612 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2613 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2614
Edward Lemur1b52d872019-05-09 21:12:12 +00002615 git_push_metadata = {
2616 'gerrit_host': self._GetGerritHost(),
2617 'title': title or '<untitled>',
2618 'change_id': change_id,
2619 'description': change_desc.description,
2620 }
2621 push_stdout = self._RunGitPushWithTraces(
2622 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002623
2624 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002625 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002626 change_numbers = [m.group(1)
2627 for m in map(regex.match, push_stdout.splitlines())
2628 if m]
2629 if len(change_numbers) != 1:
2630 DieWithError(
2631 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002632 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002633 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002634 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002635
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002636 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002637 # GetIssue() is not set in case of non-squash uploads according to tests.
2638 # TODO(agable): non-squash uploads in git cl should be removed.
2639 gerrit_util.AddReviewers(
2640 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002641 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002642 reviewers, cc,
2643 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002644
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002645 return 0
2646
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002647 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2648 change_desc):
2649 """Computes parent of the generated commit to be uploaded to Gerrit.
2650
2651 Returns revision or a ref name.
2652 """
2653 if custom_cl_base:
2654 # Try to avoid creating additional unintended CLs when uploading, unless
2655 # user wants to take this risk.
2656 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2657 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2658 local_ref_of_target_remote])
2659 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002660 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002661 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2662 'If you proceed with upload, more than 1 CL may be created by '
2663 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2664 'If you are certain that specified base `%s` has already been '
2665 'uploaded to Gerrit as another CL, you may proceed.\n' %
2666 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2667 if not force:
2668 confirm_or_exit(
2669 'Do you take responsibility for cleaning up potential mess '
2670 'resulting from proceeding with upload?',
2671 action='upload')
2672 return custom_cl_base
2673
Aaron Gablef97e33d2017-03-30 15:44:27 -07002674 if remote != '.':
2675 return self.GetCommonAncestorWithUpstream()
2676
2677 # If our upstream branch is local, we base our squashed commit on its
2678 # squashed version.
2679 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2680
Aaron Gablef97e33d2017-03-30 15:44:27 -07002681 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002682 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002683
2684 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002685 # TODO(tandrii): consider checking parent change in Gerrit and using its
2686 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2687 # the tree hash of the parent branch. The upside is less likely bogus
2688 # requests to reupload parent change just because it's uploadhash is
2689 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002690 parent = RunGit(['config',
2691 'branch.%s.gerritsquashhash' % upstream_branch_name],
2692 error_ok=True).strip()
2693 # Verify that the upstream branch has been uploaded too, otherwise
2694 # Gerrit will create additional CLs when uploading.
2695 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2696 RunGitSilent(['rev-parse', parent + ':'])):
2697 DieWithError(
2698 '\nUpload upstream branch %s first.\n'
2699 'It is likely that this branch has been rebased since its last '
2700 'upload, so you just need to upload it again.\n'
2701 '(If you uploaded it with --no-squash, then branch dependencies '
2702 'are not supported, and you should reupload with --squash.)'
2703 % upstream_branch_name,
2704 change_desc)
2705 return parent
2706
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002707 def _AddChangeIdToCommitMessage(self, options, args):
2708 """Re-commits using the current message, assumes the commit hook is in
2709 place.
2710 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002711 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002712 git_command = ['commit', '--amend', '-m', log_desc]
2713 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002714 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002715 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002716 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002717 return new_log_desc
2718 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002719 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720
tandriie113dfd2016-10-11 10:20:12 -07002721 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002722 try:
2723 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002724 except GerritChangeNotExists:
2725 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002726
2727 if data['status'] in ('ABANDONED', 'MERGED'):
2728 return 'CL %s is closed' % self.GetIssue()
2729
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002730 def GetGerritChange(self, patchset=None):
2731 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lesmesf6a22322019-11-04 22:14:39 +00002732 host = urlparse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002733 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002734 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002735 data = self._GetChangeDetail(['ALL_REVISIONS'])
2736
2737 assert host and issue and patchset, 'CL must be uploaded first'
2738
2739 has_patchset = any(
2740 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002741 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002742 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002743 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002744 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002745
tandrii8c5a3532016-11-04 07:52:02 -07002746 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002747 'host': host,
2748 'change': issue,
2749 'project': data['project'],
2750 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002751 }
tandriie113dfd2016-10-11 10:20:12 -07002752
tandriide281ae2016-10-12 06:02:30 -07002753 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002754 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002755
Edward Lemur707d70b2018-02-07 00:50:14 +01002756 def GetReviewers(self):
2757 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002758 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002759
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002760
2761_CODEREVIEW_IMPLEMENTATIONS = {
Edward Lemur125d60a2019-09-13 18:25:41 +00002762 'gerrit': Changelist,
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002763}
2764
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002765
iannuccie53c9352016-08-17 14:40:40 -07002766def _add_codereview_issue_select_options(parser, extra=""):
2767 _add_codereview_select_options(parser)
2768
2769 text = ('Operate on this issue number instead of the current branch\'s '
2770 'implicit issue.')
2771 if extra:
2772 text += ' '+extra
2773 parser.add_option('-i', '--issue', type=int, help=text)
2774
2775
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002776def _add_codereview_select_options(parser):
Edward Lemurf38bc172019-09-03 21:02:13 +00002777 """Appends --gerrit option to force specific codereview."""
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002778 parser.codereview_group = optparse.OptionGroup(
Edward Lemurf38bc172019-09-03 21:02:13 +00002779 parser, 'DEPRECATED! Codereview override options')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002780 parser.add_option_group(parser.codereview_group)
2781 parser.codereview_group.add_option(
2782 '--gerrit', action='store_true',
Edward Lemurf38bc172019-09-03 21:02:13 +00002783 help='Deprecated. Noop. Do not use.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002784
2785
tandriif9aefb72016-07-01 09:06:51 -07002786def _get_bug_line_values(default_project, bugs):
2787 """Given default_project and comma separated list of bugs, yields bug line
2788 values.
2789
2790 Each bug can be either:
2791 * a number, which is combined with default_project
2792 * string, which is left as is.
2793
2794 This function may produce more than one line, because bugdroid expects one
2795 project per line.
2796
2797 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2798 ['v8:123', 'chromium:789']
2799 """
2800 default_bugs = []
2801 others = []
2802 for bug in bugs.split(','):
2803 bug = bug.strip()
2804 if bug:
2805 try:
2806 default_bugs.append(int(bug))
2807 except ValueError:
2808 others.append(bug)
2809
2810 if default_bugs:
2811 default_bugs = ','.join(map(str, default_bugs))
2812 if default_project:
2813 yield '%s:%s' % (default_project, default_bugs)
2814 else:
2815 yield default_bugs
2816 for other in sorted(others):
2817 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2818 yield other
2819
2820
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002821class ChangeDescription(object):
2822 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002823 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002824 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002825 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00002826 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002827 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002828 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2829 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2830 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2831 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002832
Dan Beamd8b04ca2019-10-10 21:23:26 +00002833 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002834 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002835 if bug:
2836 regexp = re.compile(self.BUG_LINE)
2837 prefix = settings.GetBugPrefix()
2838 if not any((regexp.match(line) for line in self._description_lines)):
2839 values = list(_get_bug_line_values(prefix, bug))
2840 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00002841 if fixed:
2842 regexp = re.compile(self.FIXED_LINE)
2843 prefix = settings.GetBugPrefix()
2844 if not any((regexp.match(line) for line in self._description_lines)):
2845 values = list(_get_bug_line_values(prefix, fixed))
2846 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002847
agable@chromium.org42c20792013-09-12 17:34:49 +00002848 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002849 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002850 return '\n'.join(self._description_lines)
2851
2852 def set_description(self, desc):
2853 if isinstance(desc, basestring):
2854 lines = desc.splitlines()
2855 else:
2856 lines = [line.rstrip() for line in desc]
2857 while lines and not lines[0]:
2858 lines.pop(0)
2859 while lines and not lines[-1]:
2860 lines.pop(-1)
2861 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002862
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002863 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2864 """Rewrites the R=/TBR= line(s) as a single line each.
2865
2866 Args:
2867 reviewers (list(str)) - list of additional emails to use for reviewers.
2868 tbrs (list(str)) - list of additional emails to use for TBRs.
2869 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2870 the change that are missing OWNER coverage. If this is not None, you
2871 must also pass a value for `change`.
2872 change (Change) - The Change that should be used for OWNERS lookups.
2873 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002874 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002875 assert isinstance(tbrs, list), tbrs
2876
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002877 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002878 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002879
2880 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002881 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002882
2883 reviewers = set(reviewers)
2884 tbrs = set(tbrs)
2885 LOOKUP = {
2886 'TBR': tbrs,
2887 'R': reviewers,
2888 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002889
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002890 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002891 regexp = re.compile(self.R_LINE)
2892 matches = [regexp.match(line) for line in self._description_lines]
2893 new_desc = [l for i, l in enumerate(self._description_lines)
2894 if not matches[i]]
2895 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002896
agable@chromium.org42c20792013-09-12 17:34:49 +00002897 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002898
2899 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002900 for match in matches:
2901 if not match:
2902 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002903 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2904
2905 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002906 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002907 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02002908 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002909 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002910 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002911 LOOKUP[add_owners_to].update(
2912 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002913
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002914 # If any folks ended up in both groups, remove them from tbrs.
2915 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002916
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002917 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2918 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002919
2920 # Put the new lines in the description where the old first R= line was.
2921 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2922 if 0 <= line_loc < len(self._description_lines):
2923 if new_tbr_line:
2924 self._description_lines.insert(line_loc, new_tbr_line)
2925 if new_r_line:
2926 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002927 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002928 if new_r_line:
2929 self.append_footer(new_r_line)
2930 if new_tbr_line:
2931 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002932
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002933 def set_preserve_tryjobs(self):
2934 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2935 footers = git_footers.parse_footers(self.description)
2936 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2937 if v.lower() == 'true':
2938 return
2939 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2940
Anthony Polito8b955342019-09-24 19:01:36 +00002941 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002942 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002943 self.set_description([
2944 '# Enter a description of the change.',
2945 '# This will be displayed on the codereview site.',
2946 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002947 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002948 '--------------------',
2949 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00002950 bug_regexp = re.compile(self.BUG_LINE)
2951 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002952 prefix = settings.GetBugPrefix()
Dan Beamd8b04ca2019-10-10 21:23:26 +00002953 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
2954 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002955 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002956
agable@chromium.org42c20792013-09-12 17:34:49 +00002957 content = gclient_utils.RunEditor(self.description, True,
Anthony Polito8b955342019-09-24 19:01:36 +00002958 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002959 if not content:
2960 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002961 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002962
Bruce Dawson2377b012018-01-11 16:46:49 -08002963 # Strip off comments and default inserted "Bug:" line.
2964 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002965 (line.startswith('#') or
2966 line.rstrip() == "Bug:" or
2967 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002968 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002969 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002970 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002971
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002972 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002973 """Adds a footer line to the description.
2974
2975 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2976 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2977 that Gerrit footers are always at the end.
2978 """
2979 parsed_footer_line = git_footers.parse_footer(line)
2980 if parsed_footer_line:
2981 # Line is a gerrit footer in the form: Footer-Key: any value.
2982 # Thus, must be appended observing Gerrit footer rules.
2983 self.set_description(
2984 git_footers.add_footer(self.description,
2985 key=parsed_footer_line[0],
2986 value=parsed_footer_line[1]))
2987 return
2988
2989 if not self._description_lines:
2990 self._description_lines.append(line)
2991 return
2992
2993 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2994 if gerrit_footers:
2995 # git_footers.split_footers ensures that there is an empty line before
2996 # actual (gerrit) footers, if any. We have to keep it that way.
2997 assert top_lines and top_lines[-1] == ''
2998 top_lines, separator = top_lines[:-1], top_lines[-1:]
2999 else:
3000 separator = [] # No need for separator if there are no gerrit_footers.
3001
3002 prev_line = top_lines[-1] if top_lines else ''
3003 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3004 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3005 top_lines.append('')
3006 top_lines.append(line)
3007 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003008
tandrii99a72f22016-08-17 14:33:24 -07003009 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003010 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003011 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003012 reviewers = [match.group(2).strip()
3013 for match in matches
3014 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003015 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003016
bradnelsond975b302016-10-23 12:20:23 -07003017 def get_cced(self):
3018 """Retrieves the list of reviewers."""
3019 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3020 cced = [match.group(2).strip() for match in matches if match]
3021 return cleanup_list(cced)
3022
Nodir Turakulov23b82142017-11-16 11:04:25 -08003023 def get_hash_tags(self):
3024 """Extracts and sanitizes a list of Gerrit hashtags."""
3025 subject = (self._description_lines or ('',))[0]
3026 subject = re.sub(
3027 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3028
3029 tags = []
3030 start = 0
3031 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3032 while True:
3033 m = bracket_exp.match(subject, start)
3034 if not m:
3035 break
3036 tags.append(self.sanitize_hash_tag(m.group(1)))
3037 start = m.end()
3038
3039 if not tags:
3040 # Try "Tag: " prefix.
3041 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3042 if m:
3043 tags.append(self.sanitize_hash_tag(m.group(1)))
3044 return tags
3045
3046 @classmethod
3047 def sanitize_hash_tag(cls, tag):
3048 """Returns a sanitized Gerrit hash tag.
3049
3050 A sanitized hashtag can be used as a git push refspec parameter value.
3051 """
3052 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3053
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003054 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3055 """Updates this commit description given the parent.
3056
3057 This is essentially what Gnumbd used to do.
3058 Consult https://goo.gl/WMmpDe for more details.
3059 """
3060 assert parent_msg # No, orphan branch creation isn't supported.
3061 assert parent_hash
3062 assert dest_ref
3063 parent_footer_map = git_footers.parse_footers(parent_msg)
3064 # This will also happily parse svn-position, which GnumbD is no longer
3065 # supporting. While we'd generate correct footers, the verifier plugin
3066 # installed in Gerrit will block such commit (ie git push below will fail).
3067 parent_position = git_footers.get_position(parent_footer_map)
3068
3069 # Cherry-picks may have last line obscuring their prior footers,
3070 # from git_footers perspective. This is also what Gnumbd did.
3071 cp_line = None
3072 if (self._description_lines and
3073 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3074 cp_line = self._description_lines.pop()
3075
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003076 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003077
3078 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3079 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003080 for i, line in enumerate(footer_lines):
3081 k, v = git_footers.parse_footer(line) or (None, None)
3082 if k and k.startswith('Cr-'):
3083 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003084
3085 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003086 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003087 if parent_position[0] == dest_ref:
3088 # Same branch as parent.
3089 number = int(parent_position[1]) + 1
3090 else:
3091 number = 1 # New branch, and extra lineage.
3092 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3093 int(parent_position[1])))
3094
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003095 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3096 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003097
3098 self._description_lines = top_lines
3099 if cp_line:
3100 self._description_lines.append(cp_line)
3101 if self._description_lines[-1] != '':
3102 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003103 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003104
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003105
Aaron Gablea1bab272017-04-11 16:38:18 -07003106def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003107 """Retrieves the reviewers that approved a CL from the issue properties with
3108 messages.
3109
3110 Note that the list may contain reviewers that are not committer, thus are not
3111 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003112
3113 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003114 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003115 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003116 return sorted(
3117 set(
3118 message['sender']
3119 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003120 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003121 )
3122 )
3123
3124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003125def FindCodereviewSettingsFile(filename='codereview.settings'):
3126 """Finds the given file starting in the cwd and going up.
3127
3128 Only looks up to the top of the repository unless an
3129 'inherit-review-settings-ok' file exists in the root of the repository.
3130 """
3131 inherit_ok_file = 'inherit-review-settings-ok'
3132 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003133 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003134 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3135 root = '/'
3136 while True:
3137 if filename in os.listdir(cwd):
3138 if os.path.isfile(os.path.join(cwd, filename)):
3139 return open(os.path.join(cwd, filename))
3140 if cwd == root:
3141 break
3142 cwd = os.path.dirname(cwd)
3143
3144
3145def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003146 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003147 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003148
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003149 def SetProperty(name, setting, unset_error_ok=False):
3150 fullname = 'rietveld.' + name
3151 if setting in keyvals:
3152 RunGit(['config', fullname, keyvals[setting]])
3153 else:
3154 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3155
tandrii48df5812016-10-17 03:55:37 -07003156 if not keyvals.get('GERRIT_HOST', False):
3157 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003158 # Only server setting is required. Other settings can be absent.
3159 # In that case, we ignore errors raised during option deletion attempt.
3160 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3161 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3162 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003163 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003164 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3165 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003166 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3167 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003168 SetProperty(
3169 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003170
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003171 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003172 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003173
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003174 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003175 RunGit(['config', 'gerrit.squash-uploads',
3176 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003177
tandrii@chromium.org28253532016-04-14 13:46:56 +00003178 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003179 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003180 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003182 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003183 # should be of the form
3184 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3185 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3187 keyvals['ORIGIN_URL_CONFIG']])
3188
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003189
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003190def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003191 """Downloads a network object to a local file, like urllib.urlretrieve.
3192
3193 This is necessary because urllib is broken for SSL connections via a proxy.
3194 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003195 with open(destination, 'w') as f:
Edward Lesmesf6a22322019-11-04 22:14:39 +00003196 f.write(urllib_request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003197
3198
ukai@chromium.org712d6102013-11-27 00:52:58 +00003199def hasSheBang(fname):
3200 """Checks fname is a #! script."""
3201 with open(fname) as f:
3202 return f.read(2).startswith('#!')
3203
3204
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003205# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3206def DownloadHooks(*args, **kwargs):
3207 pass
3208
3209
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003210def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003211 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003212
3213 Args:
3214 force: True to update hooks. False to install hooks if not present.
3215 """
3216 if not settings.GetIsGerrit():
3217 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003218 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003219 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3220 if not os.access(dst, os.X_OK):
3221 if os.path.exists(dst):
3222 if not force:
3223 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003224 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003225 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003226 if not hasSheBang(dst):
3227 DieWithError('Not a script: %s\n'
3228 'You need to download from\n%s\n'
3229 'into .git/hooks/commit-msg and '
3230 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003231 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3232 except Exception:
3233 if os.path.exists(dst):
3234 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003235 DieWithError('\nFailed to download hooks.\n'
3236 'You need to download from\n%s\n'
3237 'into .git/hooks/commit-msg and '
3238 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003239
3240
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003241class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003242 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003243
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003244 _GOOGLESOURCE = 'googlesource.com'
3245
3246 def __init__(self):
3247 # Cached list of [host, identity, source], where source is either
3248 # .gitcookies or .netrc.
3249 self._all_hosts = None
3250
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003251 def ensure_configured_gitcookies(self):
3252 """Runs checks and suggests fixes to make git use .gitcookies from default
3253 path."""
3254 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3255 configured_path = RunGitSilent(
3256 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003257 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003258 if configured_path:
3259 self._ensure_default_gitcookies_path(configured_path, default)
3260 else:
3261 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003262
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003263 @staticmethod
3264 def _ensure_default_gitcookies_path(configured_path, default_path):
3265 assert configured_path
3266 if configured_path == default_path:
3267 print('git is already configured to use your .gitcookies from %s' %
3268 configured_path)
3269 return
3270
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003271 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003272 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3273 (configured_path, default_path))
3274
3275 if not os.path.exists(configured_path):
3276 print('However, your configured .gitcookies file is missing.')
3277 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3278 action='reconfigure')
3279 RunGit(['config', '--global', 'http.cookiefile', default_path])
3280 return
3281
3282 if os.path.exists(default_path):
3283 print('WARNING: default .gitcookies file already exists %s' %
3284 default_path)
3285 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3286 default_path)
3287
3288 confirm_or_exit('Move existing .gitcookies to default location?',
3289 action='move')
3290 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003291 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003292 print('Moved and reconfigured git to use .gitcookies from %s' %
3293 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003294
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003295 @staticmethod
3296 def _configure_gitcookies_path(default_path):
3297 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3298 if os.path.exists(netrc_path):
3299 print('You seem to be using outdated .netrc for git credentials: %s' %
3300 netrc_path)
3301 print('This tool will guide you through setting up recommended '
3302 '.gitcookies store for git credentials.\n'
3303 '\n'
3304 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3305 ' git config --global --unset http.cookiefile\n'
3306 ' mv %s %s.backup\n\n' % (default_path, default_path))
3307 confirm_or_exit(action='setup .gitcookies')
3308 RunGit(['config', '--global', 'http.cookiefile', default_path])
3309 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003310
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003311 def get_hosts_with_creds(self, include_netrc=False):
3312 if self._all_hosts is None:
3313 a = gerrit_util.CookiesAuthenticator()
3314 self._all_hosts = [
3315 (h, u, s)
3316 for h, u, s in itertools.chain(
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003317 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()),
3318 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items())
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003319 )
3320 if h.endswith(self._GOOGLESOURCE)
3321 ]
3322
3323 if include_netrc:
3324 return self._all_hosts
3325 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3326
3327 def print_current_creds(self, include_netrc=False):
3328 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3329 if not hosts:
3330 print('No Git/Gerrit credentials found')
3331 return
Edward Lesmesf6a22322019-11-04 22:14:39 +00003332 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003333 header = [('Host', 'User', 'Which file'),
3334 ['=' * l for l in lengths]]
3335 for row in (header + hosts):
3336 print('\t'.join((('%%+%ds' % l) % s)
3337 for l, s in zip(lengths, row)))
3338
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003339 @staticmethod
3340 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003341 """Parses identity "git-<username>.domain" into <username> and domain."""
3342 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003343 # distinguishable from sub-domains. But we do know typical domains:
3344 if identity.endswith('.chromium.org'):
3345 domain = 'chromium.org'
3346 username = identity[:-len('.chromium.org')]
3347 else:
3348 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003349 if username.startswith('git-'):
3350 username = username[len('git-'):]
3351 return username, domain
3352
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003353 def _canonical_git_googlesource_host(self, host):
3354 """Normalizes Gerrit hosts (with '-review') to Git host."""
3355 assert host.endswith(self._GOOGLESOURCE)
3356 # Prefix doesn't include '.' at the end.
3357 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3358 if prefix.endswith('-review'):
3359 prefix = prefix[:-len('-review')]
3360 return prefix + '.' + self._GOOGLESOURCE
3361
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003362 def _canonical_gerrit_googlesource_host(self, host):
3363 git_host = self._canonical_git_googlesource_host(host)
3364 prefix = git_host.split('.', 1)[0]
3365 return prefix + '-review.' + self._GOOGLESOURCE
3366
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003367 def _get_counterpart_host(self, host):
3368 assert host.endswith(self._GOOGLESOURCE)
3369 git = self._canonical_git_googlesource_host(host)
3370 gerrit = self._canonical_gerrit_googlesource_host(git)
3371 return git if gerrit == host else gerrit
3372
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003373 def has_generic_host(self):
3374 """Returns whether generic .googlesource.com has been configured.
3375
3376 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3377 """
3378 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3379 if host == '.' + self._GOOGLESOURCE:
3380 return True
3381 return False
3382
3383 def _get_git_gerrit_identity_pairs(self):
3384 """Returns map from canonic host to pair of identities (Git, Gerrit).
3385
3386 One of identities might be None, meaning not configured.
3387 """
3388 host_to_identity_pairs = {}
3389 for host, identity, _ in self.get_hosts_with_creds():
3390 canonical = self._canonical_git_googlesource_host(host)
3391 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3392 idx = 0 if canonical == host else 1
3393 pair[idx] = identity
3394 return host_to_identity_pairs
3395
3396 def get_partially_configured_hosts(self):
3397 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003398 (host if i1 else self._canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003399 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003400 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003401
3402 def get_conflicting_hosts(self):
3403 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003404 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003405 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003406 if None not in (i1, i2) and i1 != i2)
3407
3408 def get_duplicated_hosts(self):
3409 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003410 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003411
3412 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3413 'chromium.googlesource.com': 'chromium.org',
3414 'chrome-internal.googlesource.com': 'google.com',
3415 }
3416
3417 def get_hosts_with_wrong_identities(self):
3418 """Finds hosts which **likely** reference wrong identities.
3419
3420 Note: skips hosts which have conflicting identities for Git and Gerrit.
3421 """
3422 hosts = set()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003423 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.items():
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003424 pair = self._get_git_gerrit_identity_pairs().get(host)
3425 if pair and pair[0] == pair[1]:
3426 _, domain = self._parse_identity(pair[0])
3427 if domain != expected:
3428 hosts.add(host)
3429 return hosts
3430
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003431 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003432 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003433 hosts = sorted(hosts)
3434 assert hosts
3435 if extra_column_func is None:
3436 extras = [''] * len(hosts)
3437 else:
3438 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003439 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3440 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003441 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003442 lines.append(tmpl % he)
3443 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003444
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003445 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003446 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003447 yield ('.googlesource.com wildcard record detected',
3448 ['Chrome Infrastructure team recommends to list full host names '
3449 'explicitly.'],
3450 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003451
3452 dups = self.get_duplicated_hosts()
3453 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003454 yield ('The following hosts were defined twice',
3455 self._format_hosts(dups),
3456 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003457
3458 partial = self.get_partially_configured_hosts()
3459 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003460 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3461 'These hosts are missing',
3462 self._format_hosts(partial, lambda host: 'but %s defined' %
3463 self._get_counterpart_host(host)),
3464 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003465
3466 conflicting = self.get_conflicting_hosts()
3467 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003468 yield ('The following Git hosts have differing credentials from their '
3469 'Gerrit counterparts',
3470 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3471 tuple(self._get_git_gerrit_identity_pairs()[host])),
3472 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003473
3474 wrong = self.get_hosts_with_wrong_identities()
3475 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003476 yield ('These hosts likely use wrong identity',
3477 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3478 (self._get_git_gerrit_identity_pairs()[host][0],
3479 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3480 wrong)
3481
3482 def find_and_report_problems(self):
3483 """Returns True if there was at least one problem, else False."""
3484 found = False
3485 bad_hosts = set()
3486 for title, sublines, hosts in self._find_problems():
3487 if not found:
3488 found = True
3489 print('\n\n.gitcookies problem report:\n')
3490 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003491 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003492 if sublines:
3493 print()
3494 print(' %s' % '\n '.join(sublines))
3495 print()
3496
3497 if bad_hosts:
3498 assert found
3499 print(' You can manually remove corresponding lines in your %s file and '
3500 'visit the following URLs with correct account to generate '
3501 'correct credential lines:\n' %
3502 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3503 print(' %s' % '\n '.join(sorted(set(
3504 gerrit_util.CookiesAuthenticator().get_new_password_url(
3505 self._canonical_git_googlesource_host(host))
3506 for host in bad_hosts
3507 ))))
3508 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003509
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003510
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003511@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003512def CMDcreds_check(parser, args):
3513 """Checks credentials and suggests changes."""
3514 _, _ = parser.parse_args(args)
3515
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003516 # Code below checks .gitcookies. Abort if using something else.
3517 authn = gerrit_util.Authenticator.get()
3518 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3519 if isinstance(authn, gerrit_util.GceAuthenticator):
3520 DieWithError(
3521 'This command is not designed for GCE, are you on a bot?\n'
3522 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3523 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003524 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003525 'This command is not designed for bot environment. It checks '
3526 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003527
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003528 checker = _GitCookiesChecker()
3529 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003530
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003531 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003532 checker.print_current_creds(include_netrc=True)
3533
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003534 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003535 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003536 return 0
3537 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003538
3539
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003540@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003541def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003542 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003543 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3544 branch = ShortBranchName(branchref)
3545 _, args = parser.parse_args(args)
3546 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003547 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003548 return RunGit(['config', 'branch.%s.base-url' % branch],
3549 error_ok=False).strip()
3550 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003552 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3553 error_ok=False).strip()
3554
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003555
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003556def color_for_status(status):
3557 """Maps a Changelist status to color, for CMDstatus and other tools."""
3558 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003559 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003560 'waiting': Fore.BLUE,
3561 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003562 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003563 'lgtm': Fore.GREEN,
3564 'commit': Fore.MAGENTA,
3565 'closed': Fore.CYAN,
3566 'error': Fore.WHITE,
3567 }.get(status, Fore.WHITE)
3568
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003569
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003570def get_cl_statuses(changes, fine_grained, max_processes=None):
3571 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003572
3573 If fine_grained is true, this will fetch CL statuses from the server.
3574 Otherwise, simply indicate if there's a matching url for the given branches.
3575
3576 If max_processes is specified, it is used as the maximum number of processes
3577 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3578 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003579
3580 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003581 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003582 if not changes:
3583 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003584
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003585 if not fine_grained:
3586 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003587 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003588 for cl in changes:
3589 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003590 return
3591
3592 # First, sort out authentication issues.
3593 logging.debug('ensuring credentials exist')
3594 for cl in changes:
3595 cl.EnsureAuthenticated(force=False, refresh=True)
3596
3597 def fetch(cl):
3598 try:
3599 return (cl, cl.GetStatus())
3600 except:
3601 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003602 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003603 raise
3604
3605 threads_count = len(changes)
3606 if max_processes:
3607 threads_count = max(1, min(threads_count, max_processes))
3608 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3609
3610 pool = ThreadPool(threads_count)
3611 fetched_cls = set()
3612 try:
3613 it = pool.imap_unordered(fetch, changes).__iter__()
3614 while True:
3615 try:
3616 cl, status = it.next(timeout=5)
3617 except multiprocessing.TimeoutError:
3618 break
3619 fetched_cls.add(cl)
3620 yield cl, status
3621 finally:
3622 pool.close()
3623
3624 # Add any branches that failed to fetch.
3625 for cl in set(changes) - fetched_cls:
3626 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003627
rmistry@google.com2dd99862015-06-22 12:22:18 +00003628
3629def upload_branch_deps(cl, args):
3630 """Uploads CLs of local branches that are dependents of the current branch.
3631
3632 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003633
3634 test1 -> test2.1 -> test3.1
3635 -> test3.2
3636 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003637
3638 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3639 run on the dependent branches in this order:
3640 test2.1, test3.1, test3.2, test2.2, test3.3
3641
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003642 Note: This function does not rebase your local dependent branches. Use it
3643 when you make a change to the parent branch that will not conflict
3644 with its dependent branches, and you would like their dependencies
3645 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003646 """
3647 if git_common.is_dirty_git_tree('upload-branch-deps'):
3648 return 1
3649
3650 root_branch = cl.GetBranch()
3651 if root_branch is None:
3652 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3653 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003654 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003655 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3656 'patchset dependencies without an uploaded CL.')
3657
3658 branches = RunGit(['for-each-ref',
3659 '--format=%(refname:short) %(upstream:short)',
3660 'refs/heads'])
3661 if not branches:
3662 print('No local branches found.')
3663 return 0
3664
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003665 # Create a dictionary of all local branches to the branches that are
3666 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003667 tracked_to_dependents = collections.defaultdict(list)
3668 for b in branches.splitlines():
3669 tokens = b.split()
3670 if len(tokens) == 2:
3671 branch_name, tracked = tokens
3672 tracked_to_dependents[tracked].append(branch_name)
3673
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print()
3675 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003676 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003677
rmistry@google.com2dd99862015-06-22 12:22:18 +00003678 def traverse_dependents_preorder(branch, padding=''):
3679 dependents_to_process = tracked_to_dependents.get(branch, [])
3680 padding += ' '
3681 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003682 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003683 dependents.append(dependent)
3684 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003685
rmistry@google.com2dd99862015-06-22 12:22:18 +00003686 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003687 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003688
3689 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003690 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003691 return 0
3692
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003693 confirm_or_exit('This command will checkout all dependent branches and run '
3694 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003695
rmistry@google.com2dd99862015-06-22 12:22:18 +00003696 # Record all dependents that failed to upload.
3697 failures = {}
3698 # Go through all dependents, checkout the branch and upload.
3699 try:
3700 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003701 print()
3702 print('--------------------------------------')
3703 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003704 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003705 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003706 try:
3707 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003708 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003709 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003710 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003711 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003713 finally:
3714 # Swap back to the original root branch.
3715 RunGit(['checkout', '-q', root_branch])
3716
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print()
3718 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003719 for dependent_branch in dependents:
3720 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print(' %s : %s' % (dependent_branch, upload_status))
3722 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003723
3724 return 0
3725
3726
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003727@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003728def CMDarchive(parser, args):
3729 """Archives and deletes branches associated with closed changelists."""
3730 parser.add_option(
3731 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003732 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003733 parser.add_option(
3734 '-f', '--force', action='store_true',
3735 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003736 parser.add_option(
3737 '-d', '--dry-run', action='store_true',
3738 help='Skip the branch tagging and removal steps.')
3739 parser.add_option(
3740 '-t', '--notags', action='store_true',
3741 help='Do not tag archived branches. '
3742 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003743
kmarshall3bff56b2016-06-06 18:31:47 -07003744 options, args = parser.parse_args(args)
3745 if args:
3746 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003747
3748 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3749 if not branches:
3750 return 0
3751
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003753 changes = [Changelist(branchref=b)
3754 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003755 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3756 statuses = get_cl_statuses(changes,
3757 fine_grained=True,
3758 max_processes=options.maxjobs)
3759 proposal = [(cl.GetBranch(),
3760 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3761 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003762 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003763 proposal.sort()
3764
3765 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003766 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003767 return 0
3768
3769 current_branch = GetCurrentBranch()
3770
vapiera7fbd5a2016-06-16 09:17:49 -07003771 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003772 if options.notags:
3773 for next_item in proposal:
3774 print(' ' + next_item[0])
3775 else:
3776 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3777 for next_item in proposal:
3778 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003779
kmarshall9249e012016-08-23 12:02:16 -07003780 # Quit now on precondition failure or if instructed by the user, either
3781 # via an interactive prompt or by command line flags.
3782 if options.dry_run:
3783 print('\nNo changes were made (dry run).\n')
3784 return 0
3785 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003786 print('You are currently on a branch \'%s\' which is associated with a '
3787 'closed codereview issue, so archive cannot proceed. Please '
3788 'checkout another branch and run this command again.' %
3789 current_branch)
3790 return 1
kmarshall9249e012016-08-23 12:02:16 -07003791 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003792 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3793 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003794 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003795 return 1
3796
3797 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003798 if not options.notags:
3799 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003800 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003801
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003803
3804 return 0
3805
3806
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003807@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003808def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003809 """Show status of changelists.
3810
3811 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003812 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003813 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003814 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003815 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003816 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003817 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003818 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003819
3820 Also see 'git cl comments'.
3821 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003822 parser.add_option(
3823 '--no-branch-color',
3824 action='store_true',
3825 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003827 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003828 parser.add_option('-f', '--fast', action='store_true',
3829 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003830 parser.add_option(
3831 '-j', '--maxjobs', action='store', type=int,
3832 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003833
iannuccie53c9352016-08-17 14:40:40 -07003834 _add_codereview_issue_select_options(
3835 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003836 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003837 if args:
3838 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003839
iannuccie53c9352016-08-17 14:40:40 -07003840 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003841 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07003842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003844 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003846 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003847 elif options.field == 'id':
3848 issueid = cl.GetIssue()
3849 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003850 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003851 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003852 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003854 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003855 elif options.field == 'status':
3856 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003857 elif options.field == 'url':
3858 url = cl.GetIssueURL()
3859 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003860 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003861 return 0
3862
3863 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3864 if not branches:
3865 print('No local branch found.')
3866 return 0
3867
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003868 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003869 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003870 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003871 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003872 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003873 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003874 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003875
Daniel McArdlea23bf592019-02-12 00:25:12 +00003876 current_branch = GetCurrentBranch()
3877
3878 def FormatBranchName(branch, colorize=False):
3879 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3880 an asterisk when it is the current branch."""
3881
3882 asterisk = ""
3883 color = Fore.RESET
3884 if branch == current_branch:
3885 asterisk = "* "
3886 color = Fore.GREEN
3887 branch_name = ShortBranchName(branch)
3888
3889 if colorize:
3890 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003891 return asterisk + branch_name
3892
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003893 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003894
3895 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003896 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3897 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003898 while branch not in branch_statuses:
Edward Lesmesf6a22322019-11-04 22:14:39 +00003899 c, status = output.next()
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003900 branch_statuses[c.GetBranch()] = status
3901 status = branch_statuses.pop(branch)
3902 url = cl.GetIssueURL()
3903 if url and (not status or status == 'error'):
3904 # The issue probably doesn't exist anymore.
3905 url += ' (broken)'
3906
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003907 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003908 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003909 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003910 color = ''
3911 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003912 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003913
Alan Cuttera3be9a52019-03-04 18:50:33 +00003914 branch_display = FormatBranchName(branch)
3915 padding = ' ' * (alignment - len(branch_display))
3916 if not options.no_branch_color:
3917 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003918
Alan Cuttera3be9a52019-03-04 18:50:33 +00003919 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3920 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003921
vapiera7fbd5a2016-06-16 09:17:49 -07003922 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003923 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003924 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003925 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003926 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003927 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003928 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003929 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003931 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003932 print('Issue description:')
3933 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003934 return 0
3935
3936
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003937def colorize_CMDstatus_doc():
3938 """To be called once in main() to add colors to git cl status help."""
3939 colors = [i for i in dir(Fore) if i[0].isupper()]
3940
3941 def colorize_line(line):
3942 for color in colors:
3943 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003944 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003945 indent = len(line) - len(line.lstrip(' ')) + 1
3946 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3947 return line
3948
3949 lines = CMDstatus.__doc__.splitlines()
3950 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3951
3952
phajdan.jre328cf92016-08-22 04:12:17 -07003953def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003954 if path == '-':
3955 json.dump(contents, sys.stdout)
3956 else:
3957 with open(path, 'w') as f:
3958 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003959
3960
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003961@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003962@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003963def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003964 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003965
3966 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003967 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003968 parser.add_option('-r', '--reverse', action='store_true',
3969 help='Lookup the branch(es) for the specified issues. If '
3970 'no issues are specified, all branches with mapped '
3971 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003972 parser.add_option('--json',
3973 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003974 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003975 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003976
dnj@chromium.org406c4402015-03-03 17:22:28 +00003977 if options.reverse:
3978 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003979 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003980 # Reverse issue lookup.
3981 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003982
3983 git_config = {}
3984 for config in RunGit(['config', '--get-regexp',
3985 r'branch\..*issue']).splitlines():
3986 name, _space, val = config.partition(' ')
3987 git_config[name] = val
3988
dnj@chromium.org406c4402015-03-03 17:22:28 +00003989 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003990 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
3991 config_key = _git_branch_config_key(ShortBranchName(branch),
3992 cls.IssueConfigKey())
3993 issue = git_config.get(config_key)
3994 if issue:
3995 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003996 if not args:
3997 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003998 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003999 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004000 try:
4001 issue_num = int(issue)
4002 except ValueError:
4003 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004004 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004005 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004006 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004007 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004008 if options.json:
4009 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004010 return 0
4011
4012 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004013 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004014 if not issue.valid:
4015 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4016 'or no argument to list it.\n'
4017 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004018 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004019 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004020 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004021 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004022 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4023 if options.json:
4024 write_json(options.json, {
4025 'issue': cl.GetIssue(),
4026 'issue_url': cl.GetIssueURL(),
4027 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004028 return 0
4029
4030
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004031@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004032def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004033 """Shows or posts review comments for any changelist."""
4034 parser.add_option('-a', '--add-comment', dest='comment',
4035 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004036 parser.add_option('-p', '--publish', action='store_true',
4037 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004038 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004039 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004040 parser.add_option('-m', '--machine-readable', dest='readable',
4041 action='store_false', default=True,
4042 help='output comments in a format compatible with '
4043 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004044 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004045 help='File to write JSON summary to, or "-" for stdout')
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004046 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004047 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004048
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004049 issue = None
4050 if options.issue:
4051 try:
4052 issue = int(options.issue)
4053 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004054 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004055
Edward Lemur934836a2019-09-09 20:16:54 +00004056 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004057
4058 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004059 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004060 return 0
4061
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004062 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4063 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004064 for comment in summary:
4065 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004066 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004067 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004068 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004069 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004070 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004071 elif comment.autogenerated:
4072 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004073 else:
4074 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004075 print('\n%s%s %s%s\n%s' % (
4076 color,
4077 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4078 comment.sender,
4079 Fore.RESET,
4080 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4081
smut@google.comc85ac942015-09-15 16:34:43 +00004082 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004083 def pre_serialize(c):
Edward Lesmesf6a22322019-11-04 22:14:39 +00004084 dct = c.__dict__.copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004085 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4086 return dct
Edward Lesmesf6a22322019-11-04 22:14:39 +00004087 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004088 return 0
4089
4090
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004091@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004092@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004093def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004094 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004095 parser.add_option('-d', '--display', action='store_true',
4096 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004097 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004098 help='New description to set for this issue (- for stdin, '
4099 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004100 parser.add_option('-f', '--force', action='store_true',
4101 help='Delete any unpublished Gerrit edits for this issue '
4102 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004103
4104 _add_codereview_select_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004105 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004106
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004107 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004108 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004109 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004110 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004111 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004112
Edward Lemur934836a2019-09-09 20:16:54 +00004113 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004114 if target_issue_arg:
4115 kwargs['issue'] = target_issue_arg.issue
4116 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004117
4118 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004119 if not cl.GetIssue():
4120 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004121
Edward Lemur678a6842019-10-03 22:25:05 +00004122 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004123 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004124
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004125 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004126
smut@google.com34fb6b12015-07-13 20:03:26 +00004127 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004128 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004129 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004130
4131 if options.new_description:
4132 text = options.new_description
4133 if text == '-':
4134 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004135 elif text == '+':
4136 base_branch = cl.GetCommonAncestorWithUpstream()
4137 change = cl.GetChange(base_branch, None, local_description=True)
4138 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004139
4140 description.set_description(text)
4141 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004142 description.prompt()
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004143 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004144 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004145 return 0
4146
4147
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004148@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004149def CMDlint(parser, args):
4150 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004151 parser.add_option('--filter', action='append', metavar='-x,+y',
4152 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004153 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004154
4155 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004156 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004157 try:
4158 import cpplint
4159 import cpplint_chromium
4160 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004162 return 1
4163
4164 # Change the current working directory before calling lint so that it
4165 # shows the correct base.
4166 previous_cwd = os.getcwd()
4167 os.chdir(settings.GetRoot())
4168 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004169 cl = Changelist()
thestig@chromium.org44202a22014-03-11 19:22:18 +00004170 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4171 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004172 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004173 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004174 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004175
4176 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004177 command = args + files
4178 if options.filter:
4179 command = ['--filter=' + ','.join(options.filter)] + command
4180 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004181
4182 white_regex = re.compile(settings.GetLintRegex())
4183 black_regex = re.compile(settings.GetLintIgnoreRegex())
4184 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4185 for filename in filenames:
4186 if white_regex.match(filename):
4187 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004189 else:
4190 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4191 extra_check_functions)
4192 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004193 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004194 finally:
4195 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004197 if cpplint._cpplint_state.error_count != 0:
4198 return 1
4199 return 0
4200
4201
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004202@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004204 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004205 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004206 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004207 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004208 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004209 parser.add_option('--all', action='store_true',
4210 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004211 parser.add_option('--parallel', action='store_true',
4212 help='Run all tests specified by input_api.RunTests in all '
4213 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004214 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004215
sbc@chromium.org71437c02015-04-09 19:29:40 +00004216 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004217 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004218 return 1
4219
Edward Lemur934836a2019-09-09 20:16:54 +00004220 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221 if args:
4222 base_branch = args[0]
4223 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004224 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004225 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226
Aaron Gable8076c282017-11-29 14:39:41 -08004227 if options.all:
4228 base_change = cl.GetChange(base_branch, None)
4229 files = [('M', f) for f in base_change.AllFiles()]
4230 change = presubmit_support.GitChange(
4231 base_change.Name(),
4232 base_change.FullDescriptionText(),
4233 base_change.RepositoryRoot(),
4234 files,
4235 base_change.issue,
4236 base_change.patchset,
4237 base_change.author_email,
4238 base_change._upstream)
4239 else:
4240 change = cl.GetChange(base_branch, None)
4241
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004242 cl.RunHook(
4243 committing=not options.upload,
4244 may_prompt=False,
4245 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004246 change=change,
4247 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004248 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249
4250
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004251def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004252 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004253
4254 Works the same way as
4255 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4256 but can be called on demand on all platforms.
4257
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004258 The basic idea is to generate git hash of a state of the tree, original
4259 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004260 """
4261 lines = []
4262 tree_hash = RunGitSilent(['write-tree'])
4263 lines.append('tree %s' % tree_hash.strip())
4264 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4265 if code == 0:
4266 lines.append('parent %s' % parent.strip())
4267 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4268 lines.append('author %s' % author.strip())
4269 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4270 lines.append('committer %s' % committer.strip())
4271 lines.append('')
4272 # Note: Gerrit's commit-hook actually cleans message of some lines and
4273 # whitespace. This code is not doing this, but it clearly won't decrease
4274 # entropy.
4275 lines.append(message)
4276 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004277 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004278 return 'I%s' % change_hash.strip()
4279
4280
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004281def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004282 """Computes the remote branch ref to use for the CL.
4283
4284 Args:
4285 remote (str): The git remote for the CL.
4286 remote_branch (str): The git remote branch for the CL.
4287 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004288 """
4289 if not (remote and remote_branch):
4290 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004291
wittman@chromium.org455dc922015-01-26 20:15:50 +00004292 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004293 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004294 # refs, which are then translated into the remote full symbolic refs
4295 # below.
4296 if '/' not in target_branch:
4297 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4298 else:
4299 prefix_replacements = (
4300 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4301 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4302 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4303 )
4304 match = None
4305 for regex, replacement in prefix_replacements:
4306 match = re.search(regex, target_branch)
4307 if match:
4308 remote_branch = target_branch.replace(match.group(0), replacement)
4309 break
4310 if not match:
4311 # This is a branch path but not one we recognize; use as-is.
4312 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004313 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4314 # Handle the refs that need to land in different refs.
4315 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004316
wittman@chromium.org455dc922015-01-26 20:15:50 +00004317 # Create the true path to the remote branch.
4318 # Does the following translation:
4319 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4320 # * refs/remotes/origin/master -> refs/heads/master
4321 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4322 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4323 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4324 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4325 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4326 'refs/heads/')
4327 elif remote_branch.startswith('refs/remotes/branch-heads'):
4328 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004329
wittman@chromium.org455dc922015-01-26 20:15:50 +00004330 return remote_branch
4331
4332
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004333def cleanup_list(l):
4334 """Fixes a list so that comma separated items are put as individual items.
4335
4336 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4337 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4338 """
4339 items = sum((i.split(',') for i in l), [])
4340 stripped_items = (i.strip() for i in items)
4341 return sorted(filter(None, stripped_items))
4342
4343
Aaron Gable4db38df2017-11-03 14:59:07 -07004344@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004345@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004346def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004347 """Uploads the current changelist to codereview.
4348
4349 Can skip dependency patchset uploads for a branch by running:
4350 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004351 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004352 git config --unset branch.branch_name.skip-deps-uploads
4353 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004354
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004355 If the name of the checked out branch starts with "bug-" or "fix-" followed
4356 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004357 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004358
4359 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004360 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004361 [git-cl] add support for hashtags
4362 Foo bar: implement foo
4363 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004364 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004365 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4366 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004367 parser.add_option('--bypass-watchlists', action='store_true',
4368 dest='bypass_watchlists',
4369 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004370 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004371 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004372 parser.add_option('--message', '-m', dest='message',
4373 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004374 parser.add_option('-b', '--bug',
4375 help='pre-populate the bug number(s) for this issue. '
4376 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004377 parser.add_option('--message-file', dest='message_file',
4378 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004379 parser.add_option('--title', '-t', dest='title',
4380 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004381 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004382 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004383 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004384 parser.add_option('--tbrs',
4385 action='append', default=[],
4386 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004387 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004388 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004389 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004390 parser.add_option('--hashtag', dest='hashtags',
4391 action='append', default=[],
4392 help=('Gerrit hashtag for new CL; '
4393 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004394 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004395 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004396 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004397 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004398 metavar='TARGET',
4399 help='Apply CL to remote ref TARGET. ' +
4400 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004401 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004402 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004403 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004404 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004405 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004406 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004407 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4408 const='TBR', help='add a set of OWNERS to TBR')
4409 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4410 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004411 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004412 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004413 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004414 'implies --send-mail')
4415 parser.add_option('-d', '--cq-dry-run',
4416 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004417 help='Send the patchset to do a CQ dry run right after '
4418 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004419 parser.add_option('--preserve-tryjobs', action='store_true',
4420 help='instruct the CQ to let tryjobs running even after '
4421 'new patchsets are uploaded instead of canceling '
4422 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004423 parser.add_option('--dependencies', action='store_true',
4424 help='Uploads CLs of all the local branches that depend on '
4425 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004426 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4427 help='Sends your change to the CQ after an approval. Only '
4428 'works on repos that have the Auto-Submit label '
4429 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004430 parser.add_option('--parallel', action='store_true',
4431 help='Run all tests specified by input_api.RunTests in all '
4432 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004433 parser.add_option('--no-autocc', action='store_true',
4434 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004435 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004436 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004437 parser.add_option('-R', '--retry-failed', action='store_true',
4438 help='Retry failed tryjobs from old patchset immediately '
4439 'after uploading new patchset. Cannot be used with '
4440 '--use-commit-queue or --cq-dry-run.')
4441 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4442 help='Host of buildbucket. The default host is %default.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004443 parser.add_option('--fixed', '-x',
4444 help='List of bugs that will be commented on and marked '
4445 'fixed (pre-populates "Fixed:" tag). Same format as '
4446 '-b option / "Bug:" tag. If fixing several issues, '
4447 'separate with commas.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004448
rmistry@google.com2dd99862015-06-22 12:22:18 +00004449 orig_args = args
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004450 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004451 (options, args) = parser.parse_args(args)
4452
sbc@chromium.org71437c02015-04-09 19:29:40 +00004453 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004454 return 1
4455
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004456 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004457 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004458 options.cc = cleanup_list(options.cc)
4459
tandriib80458a2016-06-23 12:20:07 -07004460 if options.message_file:
4461 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004462 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004463 options.message = gclient_utils.FileRead(options.message_file)
4464 options.message_file = None
4465
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004466 if ([options.cq_dry_run,
4467 options.use_commit_queue,
4468 options.retry_failed].count(True) > 1):
4469 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4470 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004471
Aaron Gableedbc4132017-09-11 13:22:28 -07004472 if options.use_commit_queue:
4473 options.send_mail = True
4474
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004475 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4476 settings.GetIsGerrit()
4477
Edward Lemur934836a2019-09-09 20:16:54 +00004478 cl = Changelist()
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004479 if options.retry_failed and not cl.GetIssue():
4480 print('No previous patchsets, so --retry-failed has no effect.')
4481 options.retry_failed = False
4482 # cl.GetMostRecentPatchset uses cached information, and can return the last
4483 # patchset before upload. Calling it here makes it clear that it's the
4484 # last patchset before upload. Note that GetMostRecentPatchset will fail
4485 # if no CL has been uploaded yet.
4486 if options.retry_failed:
4487 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004488
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004489 ret = cl.CMDUpload(options, args, orig_args)
4490
4491 if options.retry_failed:
4492 if ret != 0:
4493 print('Upload failed, so --retry-failed has no effect.')
4494 return ret
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +00004495 builds, _ = _fetch_latest_builds(
Edward Lemur5b929a42019-10-21 17:57:39 +00004496 cl, options.buildbucket_host, latest_patchset=patchset)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +00004497 buckets = _filter_failed_for_retry(builds)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004498 if len(buckets) == 0:
4499 print('No failed tryjobs, so --retry-failed has no effect.')
4500 return ret
Edward Lemur5b929a42019-10-21 17:57:39 +00004501 _trigger_try_jobs(cl, buckets, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004502
4503 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004504
4505
Francois Dorayd42c6812017-05-30 15:10:20 -04004506@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004507@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004508def CMDsplit(parser, args):
4509 """Splits a branch into smaller branches and uploads CLs.
4510
4511 Creates a branch and uploads a CL for each group of files modified in the
4512 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004513 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004514 the shared OWNERS file.
4515 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004516 parser.add_option('-d', '--description', dest='description_file',
4517 help='A text file containing a CL description in which '
4518 '$directory will be replaced by each CL\'s directory.')
4519 parser.add_option('-c', '--comment', dest='comment_file',
4520 help='A text file containing a CL comment.')
4521 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004522 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004523 help='List the files and reviewers for each CL that would '
4524 'be created, but don\'t create branches or CLs.')
4525 parser.add_option('--cq-dry-run', action='store_true',
4526 help='If set, will do a cq dry run for each uploaded CL. '
4527 'Please be careful when doing this; more than ~10 CLs '
4528 'has the potential to overload our build '
4529 'infrastructure. Try to upload these not during high '
4530 'load times (usually 11-3 Mountain View time). Email '
4531 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004532 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4533 default=True,
4534 help='Sends your change to the CQ after an approval. Only '
4535 'works on repos that have the Auto-Submit label '
4536 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004537 options, _ = parser.parse_args(args)
4538
4539 if not options.description_file:
4540 parser.error('No --description flag specified.')
4541
4542 def WrappedCMDupload(args):
4543 return CMDupload(OptionParser(), args)
4544
4545 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004546 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004547 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004548
4549
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004550@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004551@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004552def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004553 """DEPRECATED: Used to commit the current changelist via git-svn."""
4554 message = ('git-cl no longer supports committing to SVN repositories via '
4555 'git-svn. You probably want to use `git cl land` instead.')
4556 print(message)
4557 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004558
4559
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004560@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004561@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004562def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004563 """Commits the current changelist via git.
4564
4565 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4566 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004567 """
4568 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4569 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004570 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004571 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004572 parser.add_option('--parallel', action='store_true',
4573 help='Run all tests specified by input_api.RunTests in all '
4574 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004575 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004576
Edward Lemur934836a2019-09-09 20:16:54 +00004577 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004578
Robert Iannucci2e73d432018-03-14 01:10:47 -07004579 if not cl.GetIssue():
4580 DieWithError('You must upload the change first to Gerrit.\n'
4581 ' If you would rather have `git cl land` upload '
4582 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004583 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004584 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004585
4586
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004587@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004588@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004590 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591 parser.add_option('-b', dest='newbranch',
4592 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004593 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004594 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004595 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004596 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004597
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004598 group = optparse.OptionGroup(
4599 parser,
4600 'Options for continuing work on the current issue uploaded from a '
4601 'different clone (e.g. different machine). Must be used independently '
4602 'from the other options. No issue number should be specified, and the '
4603 'branch must have an issue number associated with it')
4604 group.add_option('--reapply', action='store_true', dest='reapply',
4605 help='Reset the branch and reapply the issue.\n'
4606 'CAUTION: This will undo any local changes in this '
4607 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004608
4609 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004610 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004611 parser.add_option_group(group)
4612
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004613 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004614 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004615
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004616 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004617 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004618 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004619 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004620 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004621
Edward Lemur934836a2019-09-09 20:16:54 +00004622 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004623 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004624 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004625
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004626 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004627 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004628 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004629
4630 RunGit(['reset', '--hard', upstream])
4631 if options.pull:
4632 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004633
Edward Lemur678a6842019-10-03 22:25:05 +00004634 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4635 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004636
4637 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004638 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004639
Edward Lemurf38bc172019-09-03 21:02:13 +00004640 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004641 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004642 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004643
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004644 # We don't want uncommitted changes mixed up with the patch.
4645 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004646 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004647
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004648 if options.newbranch:
4649 if options.force:
4650 RunGit(['branch', '-D', options.newbranch],
4651 stderr=subprocess2.PIPE, error_ok=True)
4652 RunGit(['new-branch', options.newbranch])
4653
Edward Lemur678a6842019-10-03 22:25:05 +00004654 cl = Changelist(
4655 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004656
Edward Lemur678a6842019-10-03 22:25:05 +00004657 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004658 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004659
Edward Lemurf38bc172019-09-03 21:02:13 +00004660 return cl.CMDPatchWithParsedIssue(
4661 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662
4663
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004664def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004665 """Fetches the tree status and returns either 'open', 'closed',
4666 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004667 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004668 if url:
Edward Lesmesf6a22322019-11-04 22:14:39 +00004669 status = urllib_request.urlopen(url).read().lower()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004670 if status.find('closed') != -1 or status == '0':
4671 return 'closed'
4672 elif status.find('open') != -1 or status == '1':
4673 return 'open'
4674 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675 return 'unset'
4676
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004677
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004678def GetTreeStatusReason():
4679 """Fetches the tree status from a json url and returns the message
4680 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004681 url = settings.GetTreeStatusUrl()
4682 json_url = urlparse.urljoin(url, '/current?format=json')
Edward Lesmesf6a22322019-11-04 22:14:39 +00004683 connection = urllib_request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004684 status = json.loads(connection.read())
4685 connection.close()
4686 return status['message']
4687
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004688
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004689@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004691 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004692 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693 status = GetTreeStatus()
4694 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004695 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696 return 2
4697
vapiera7fbd5a2016-06-16 09:17:49 -07004698 print('The tree is %s' % status)
4699 print()
4700 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004701 if status != 'open':
4702 return 1
4703 return 0
4704
4705
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004706@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004707def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004708 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4709 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004710 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004711 '-b', '--bot', action='append',
4712 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4713 'times to specify multiple builders. ex: '
4714 '"-b win_rel -b win_layout". See '
4715 'the try server waterfall for the builders name and the tests '
4716 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004717 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004718 '-B', '--bucket', default='',
4719 help=('Buildbucket bucket to send the try requests.'))
4720 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004721 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004722 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004723 'be determined by the try recipe that builder runs, which usually '
4724 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004725 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004726 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004727 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004728 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004729 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004730 '--category', default='git_cl_try', help='Specify custom build category.')
4731 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004732 '--project',
4733 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004734 'in recipe to determine to which repository or directory to '
4735 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004736 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004737 '-p', '--property', dest='properties', action='append', default=[],
4738 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004739 'key2=value2 etc. The value will be treated as '
4740 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004741 'NOTE: using this may make your tryjob not usable for CQ, '
4742 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004743 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004744 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4745 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004746 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004747 parser.add_option(
4748 '-R', '--retry-failed', action='store_true', default=False,
4749 help='Retry failed jobs from the latest set of tryjobs. '
4750 'Not allowed with --bucket and --bot options.')
Koji Ishii31c14782018-01-08 17:17:33 +09004751 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004752 options, args = parser.parse_args(args)
4753
machenbach@chromium.org45453142015-09-15 08:45:22 +00004754 # Make sure that all properties are prop=value pairs.
4755 bad_params = [x for x in options.properties if '=' not in x]
4756 if bad_params:
4757 parser.error('Got properties with missing "=": %s' % bad_params)
4758
maruel@chromium.org15192402012-09-06 12:38:29 +00004759 if args:
4760 parser.error('Unknown arguments: %s' % args)
4761
Edward Lemur934836a2019-09-09 20:16:54 +00004762 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004763 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004764 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004765
Edward Lemurf38bc172019-09-03 21:02:13 +00004766 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004767 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004768
tandriie113dfd2016-10-11 10:20:12 -07004769 error_message = cl.CannotTriggerTryJobReason()
4770 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004771 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004772
Quinten Yearsley983111f2019-09-26 17:18:48 +00004773 if options.retry_failed:
4774 if options.bot or options.bucket:
4775 print('ERROR: The option --retry-failed is not compatible with '
4776 '-B, -b, --bucket, or --bot.', file=sys.stderr)
4777 return 1
4778 print('Searching for failed tryjobs...')
Edward Lemur5b929a42019-10-21 17:57:39 +00004779 builds, patchset = _fetch_latest_builds(cl, options.buildbucket_host)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004780 if options.verbose:
4781 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +00004782 buckets = _filter_failed_for_retry(builds)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004783 if not buckets:
4784 print('There are no failed jobs in the latest set of jobs '
4785 '(patchset #%d), doing nothing.' % patchset)
4786 return 0
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00004787 num_builders = sum(map(len, buckets.values()))
Quinten Yearsley983111f2019-09-26 17:18:48 +00004788 if num_builders > 10:
4789 confirm_or_exit('There are %d builders with failed builds.'
4790 % num_builders, action='continue')
4791 else:
4792 buckets = _get_bucket_map(cl, options, parser)
4793 if buckets and any(b.startswith('master.') for b in buckets):
4794 print('ERROR: Buildbot masters are not supported.')
4795 return 1
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004796
qyearsleydd49f942016-10-28 11:57:22 -07004797 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4798 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004799 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004800 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004801 print('git cl try with no bots now defaults to CQ dry run.')
4802 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4803 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004804
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00004805 for builders in buckets.values():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004806 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004807 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004808 'of bot requires an initial job from a parent (usually a builder). '
4809 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004810 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004811 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004812
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004813 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004814 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00004815 _trigger_try_jobs(cl, buckets, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00004816 except BuildbucketResponseException as ex:
4817 print('ERROR: %s' % ex)
4818 return 1
4819 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004820
4821
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004822@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004823def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004824 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004825 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004826 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004827 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004828 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004829 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004830 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004831 '--color', action='store_true', default=setup_color.IS_TTY,
4832 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004833 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004834 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4835 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004836 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004837 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004838 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004839 parser.add_option_group(group)
Stefan Zager27db3f22017-10-10 15:15:01 -07004840 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004841 options, args = parser.parse_args(args)
4842 if args:
4843 parser.error('Unrecognized args: %s' % ' '.join(args))
4844
Edward Lemur934836a2019-09-09 20:16:54 +00004845 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004846 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004847 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004848
tandrii221ab252016-10-06 08:12:04 -07004849 patchset = options.patchset
4850 if not patchset:
4851 patchset = cl.GetMostRecentPatchset()
4852 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004853 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004854 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004855 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004856 cl.GetIssue())
4857
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004858 try:
Edward Lemur5b929a42019-10-21 17:57:39 +00004859 jobs = fetch_try_jobs(cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004860 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004861 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004862 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004863 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00004864 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07004865 else:
4866 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004867 return 0
4868
4869
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004870@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004871@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004872def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004873 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004874 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004875 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004876 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004877
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004878 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004879 if args:
4880 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004881 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004882 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004883 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004884 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004885
4886 # Clear configured merge-base, if there is one.
4887 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004888 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004889 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004890 return 0
4891
4892
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004893@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004894def CMDweb(parser, args):
4895 """Opens the current CL in the web browser."""
4896 _, args = parser.parse_args(args)
4897 if args:
4898 parser.error('Unrecognized args: %s' % ' '.join(args))
4899
4900 issue_url = Changelist().GetIssueURL()
4901 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004902 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004903 return 1
4904
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004905 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004906 # allows us to hide the "Created new window in existing browser session."
4907 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004908 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004909 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004910 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004911 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004912 os.open(os.devnull, os.O_RDWR)
4913 try:
4914 webbrowser.open(issue_url)
4915 finally:
4916 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004917 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004918 return 0
4919
4920
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004921@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004922def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004923 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004924 parser.add_option('-d', '--dry-run', action='store_true',
4925 help='trigger in dry run mode')
4926 parser.add_option('-c', '--clear', action='store_true',
4927 help='stop CQ run, if any')
iannuccie53c9352016-08-17 14:40:40 -07004928 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004929 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004930 if args:
4931 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004932 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004933 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004934
Edward Lemur934836a2019-09-09 20:16:54 +00004935 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004936 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004937 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004938 elif options.dry_run:
4939 state = _CQState.DRY_RUN
4940 else:
4941 state = _CQState.COMMIT
4942 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004943 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004944 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004945 return 0
4946
4947
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004948@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004949def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004950 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004951 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004952 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004953 if args:
4954 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004955 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004956 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004957 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004958 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004959 cl.CloseIssue()
4960 return 0
4961
4962
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004963@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004964def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004965 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004966 parser.add_option(
4967 '--stat',
4968 action='store_true',
4969 dest='stat',
4970 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004971 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004972 if args:
4973 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004974
Edward Lemur934836a2019-09-09 20:16:54 +00004975 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004976 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004977 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004978 if not issue:
4979 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004980
Aaron Gablea718c3e2017-08-28 17:47:28 -07004981 base = cl._GitGetBranchConfigValue('last-upload-hash')
4982 if not base:
4983 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4984 if not base:
4985 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4986 revision_info = detail['revisions'][detail['current_revision']]
4987 fetch_info = revision_info['fetch']['http']
4988 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4989 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004990
Aaron Gablea718c3e2017-08-28 17:47:28 -07004991 cmd = ['git', 'diff']
4992 if options.stat:
4993 cmd.append('--stat')
4994 cmd.append(base)
4995 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004996
4997 return 0
4998
4999
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005000@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005001def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005002 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005003 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005004 '--ignore-current',
5005 action='store_true',
5006 help='Ignore the CL\'s current reviewers and start from scratch.')
5007 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005008 '--ignore-self',
5009 action='store_true',
5010 help='Do not consider CL\'s author as an owners.')
5011 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005012 '--no-color',
5013 action='store_true',
5014 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005015 parser.add_option(
5016 '--batch',
5017 action='store_true',
5018 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005019 # TODO: Consider moving this to another command, since other
5020 # git-cl owners commands deal with owners for a given CL.
5021 parser.add_option(
5022 '--show-all',
5023 action='store_true',
5024 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005025 options, args = parser.parse_args(args)
5026
5027 author = RunGit(['config', 'user.email']).strip() or None
5028
Edward Lemur934836a2019-09-09 20:16:54 +00005029 cl = Changelist()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005030
Yang Guo6e269a02019-06-26 11:17:02 +00005031 if options.show_all:
5032 for arg in args:
5033 base_branch = cl.GetCommonAncestorWithUpstream()
5034 change = cl.GetChange(base_branch, None)
5035 database = owners.Database(change.RepositoryRoot(), file, os.path)
5036 database.load_data_needed_for([arg])
5037 print('Owners for %s:' % arg)
5038 for owner in sorted(database.all_possible_owners([arg], None)):
5039 print(' - %s' % owner)
5040 return 0
5041
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005042 if args:
5043 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005044 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005045 base_branch = args[0]
5046 else:
5047 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005048 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005049
5050 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005051 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5052
5053 if options.batch:
5054 db = owners.Database(change.RepositoryRoot(), file, os.path)
5055 print('\n'.join(db.reviewers_for(affected_files, author)))
5056 return 0
5057
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005058 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005059 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005060 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005061 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005062 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005063 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005064 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005065 override_files=change.OriginalOwnersFiles(),
5066 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005067
5068
Aiden Bennerc08566e2018-10-03 17:52:42 +00005069def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005070 """Generates a diff command."""
5071 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005072 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5073
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005074 if allow_prefix:
5075 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5076 # case that diff.noprefix is set in the user's git config.
5077 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5078 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005079 diff_cmd += ['--no-prefix']
5080
5081 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005082
5083 if args:
5084 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005085 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005086 diff_cmd.append(arg)
5087 else:
5088 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005089
5090 return diff_cmd
5091
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005092
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005093def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005094 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005095 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005096
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005097
enne@chromium.org555cfe42014-01-29 18:21:39 +00005098@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005099@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005100def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005101 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005102 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005103 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005104 parser.add_option('--full', action='store_true',
5105 help='Reformat the full content of all touched files')
5106 parser.add_option('--dry-run', action='store_true',
5107 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005108 parser.add_option(
5109 '--python',
5110 action='store_true',
5111 default=None,
5112 help='Enables python formatting on all python files.')
5113 parser.add_option(
5114 '--no-python',
5115 action='store_true',
5116 dest='python',
5117 help='Disables python formatting on all python files. '
5118 'Takes precedence over --python. '
5119 'If neither --python or --no-python are set, python '
5120 'files that have a .style.yapf file in an ancestor '
5121 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005122 parser.add_option('--js', action='store_true',
5123 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005124 parser.add_option('--diff', action='store_true',
5125 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005126 parser.add_option('--presubmit', action='store_true',
5127 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005128 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005129
Daniel Chengc55eecf2016-12-30 03:11:02 -08005130 # Normalize any remaining args against the current path, so paths relative to
5131 # the current directory are still resolved as expected.
5132 args = [os.path.join(os.getcwd(), arg) for arg in args]
5133
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005134 # git diff generates paths against the root of the repository. Change
5135 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005136 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005137 if rel_base_path:
5138 os.chdir(rel_base_path)
5139
digit@chromium.org29e47272013-05-17 17:01:46 +00005140 # Grab the merge-base commit, i.e. the upstream commit of the current
5141 # branch when it was created or the last time it was rebased. This is
5142 # to cover the case where the user may have called "git fetch origin",
5143 # moving the origin branch to a newer commit, but hasn't rebased yet.
5144 upstream_commit = None
5145 cl = Changelist()
5146 upstream_branch = cl.GetUpstreamBranch()
5147 if upstream_branch:
5148 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5149 upstream_commit = upstream_commit.strip()
5150
5151 if not upstream_commit:
5152 DieWithError('Could not find base commit for this branch. '
5153 'Are you in detached state?')
5154
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005155 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5156 diff_output = RunGit(changed_files_cmd)
5157 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005158 # Filter out files deleted by this CL
5159 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005160
Christopher Lamc5ba6922017-01-24 11:19:14 +11005161 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005162 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005163
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005164 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5165 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5166 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005167 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005168
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005169 top_dir = os.path.normpath(
5170 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5171
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005172 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5173 # formatted. This is used to block during the presubmit.
5174 return_value = 0
5175
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005176 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005177 # Locate the clang-format binary in the checkout
5178 try:
5179 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005180 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005181 DieWithError(e)
5182
Jamie Madilldc4d19e2019-10-24 21:50:02 +00005183 if opts.full or settings.GetFormatFullByDefault():
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005184 cmd = [clang_format_tool]
5185 if not opts.dry_run and not opts.diff:
5186 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005187 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005188 if opts.diff:
5189 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005190 else:
5191 env = os.environ.copy()
5192 env['PATH'] = str(os.path.dirname(clang_format_tool))
5193 try:
5194 script = clang_format.FindClangFormatScriptInChromiumTree(
5195 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005196 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005197 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005198
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005199 cmd = [sys.executable, script, '-p0']
5200 if not opts.dry_run and not opts.diff:
5201 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005202
Jamie Madill3671a6a2019-10-24 15:13:21 +00005203 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005204 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005205
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005206 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5207 if opts.diff:
5208 sys.stdout.write(stdout)
5209 if opts.dry_run and len(stdout) > 0:
5210 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005211
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005212 # Similar code to above, but using yapf on .py files rather than clang-format
5213 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005214 py_explicitly_disabled = opts.python is not None and not opts.python
5215 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005216 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5217 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5218 if sys.platform.startswith('win'):
5219 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005220
Aiden Bennerc08566e2018-10-03 17:52:42 +00005221 # If we couldn't find a yapf file we'll default to the chromium style
5222 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005223 chromium_default_yapf_style = os.path.join(depot_tools_path,
5224 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005225 # Used for caching.
5226 yapf_configs = {}
5227 for f in python_diff_files:
5228 # Find the yapf style config for the current file, defaults to depot
5229 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005230 _FindYapfConfigFile(f, yapf_configs, top_dir)
5231
5232 # Turn on python formatting by default if a yapf config is specified.
5233 # This breaks in the case of this repo though since the specified
5234 # style file is also the global default.
5235 if opts.python is None:
5236 filtered_py_files = []
5237 for f in python_diff_files:
5238 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5239 filtered_py_files.append(f)
5240 else:
5241 filtered_py_files = python_diff_files
5242
5243 # Note: yapf still seems to fix indentation of the entire file
5244 # even if line ranges are specified.
5245 # See https://github.com/google/yapf/issues/499
5246 if not opts.full and filtered_py_files:
5247 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5248
Brian Sheedy59b06a82019-10-14 17:03:29 +00005249 ignored_yapf_files = _GetYapfIgnoreFilepaths(top_dir)
5250
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005251 for f in filtered_py_files:
Brian Sheedy59b06a82019-10-14 17:03:29 +00005252 if f in ignored_yapf_files:
5253 continue
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005254 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5255 if yapf_config is None:
5256 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005257
5258 cmd = [yapf_tool, '--style', yapf_config, f]
5259
5260 has_formattable_lines = False
5261 if not opts.full:
5262 # Only run yapf over changed line ranges.
5263 for diff_start, diff_len in py_line_diffs[f]:
5264 diff_end = diff_start + diff_len - 1
5265 # Yapf errors out if diff_end < diff_start but this
5266 # is a valid line range diff for a removal.
5267 if diff_end >= diff_start:
5268 has_formattable_lines = True
5269 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5270 # If all line diffs were removals we have nothing to format.
5271 if not has_formattable_lines:
5272 continue
5273
5274 if opts.diff or opts.dry_run:
5275 cmd += ['--diff']
5276 # Will return non-zero exit code if non-empty diff.
5277 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5278 if opts.diff:
5279 sys.stdout.write(stdout)
5280 elif len(stdout) > 0:
5281 return_value = 2
5282 else:
5283 cmd += ['-i']
5284 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005285
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005286 # Dart's formatter does not have the nice property of only operating on
5287 # modified chunks, so hard code full.
5288 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005289 try:
5290 command = [dart_format.FindDartFmtToolInChromiumTree()]
5291 if not opts.dry_run and not opts.diff:
5292 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005293 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005294
ppi@chromium.org6593d932016-03-03 15:41:15 +00005295 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005296 if opts.dry_run and stdout:
5297 return_value = 2
5298 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005299 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5300 'found in this checkout. Files in other languages are still '
5301 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005302
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005303 # Format GN build files. Always run on full build files for canonical form.
5304 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005305 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005306 if opts.dry_run or opts.diff:
5307 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005308 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005309 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5310 shell=sys.platform == 'win32',
5311 cwd=top_dir)
5312 if opts.dry_run and gn_ret == 2:
5313 return_value = 2 # Not formatted.
5314 elif opts.diff and gn_ret == 2:
5315 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005316 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005317 elif gn_ret != 0:
5318 # For non-dry run cases (and non-2 return values for dry-run), a
5319 # nonzero error code indicates a failure, probably because the file
5320 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005321 DieWithError('gn format failed on ' + gn_diff_file +
5322 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005323
Ilya Shermane081cbe2017-08-15 17:51:04 -07005324 # Skip the metrics formatting from the global presubmit hook. These files have
5325 # a separate presubmit hook that issues an error if the files need formatting,
5326 # whereas the top-level presubmit script merely issues a warning. Formatting
5327 # these files is somewhat slow, so it's important not to duplicate the work.
5328 if not opts.presubmit:
5329 for xml_dir in GetDirtyMetricsDirs(diff_files):
5330 tool_dir = os.path.join(top_dir, xml_dir)
5331 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5332 if opts.dry_run or opts.diff:
5333 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005334 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005335 if opts.diff:
5336 sys.stdout.write(stdout)
5337 if opts.dry_run and stdout:
5338 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005339
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005340 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005341
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005342
Steven Holte2e664bf2017-04-21 13:10:47 -07005343def GetDirtyMetricsDirs(diff_files):
5344 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5345 metrics_xml_dirs = [
5346 os.path.join('tools', 'metrics', 'actions'),
5347 os.path.join('tools', 'metrics', 'histograms'),
5348 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005349 os.path.join('tools', 'metrics', 'ukm'),
5350 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005351 for xml_dir in metrics_xml_dirs:
5352 if any(file.startswith(xml_dir) for file in xml_diff_files):
5353 yield xml_dir
5354
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005355
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005356@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005357@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005358def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005359 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005360 _, args = parser.parse_args(args)
5361
5362 if len(args) != 1:
5363 parser.print_help()
5364 return 1
5365
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005366 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005367 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005368 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005369
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005370 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005371
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005372 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005373 output = RunGit(['config', '--local', '--get-regexp',
5374 r'branch\..*\.%s' % issueprefix],
5375 error_ok=True)
5376 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005377 if issue == target_issue:
5378 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005379
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005380 branches = []
5381 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005382 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005383 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005384 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005385 return 1
5386 if len(branches) == 1:
5387 RunGit(['checkout', branches[0]])
5388 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005389 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005390 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005391 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005392 which = raw_input('Choose by index: ')
5393 try:
5394 RunGit(['checkout', branches[int(which)]])
5395 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005396 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005397 return 1
5398
5399 return 0
5400
5401
maruel@chromium.org29404b52014-09-08 22:58:00 +00005402def CMDlol(parser, args):
5403 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005404 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005405 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5406 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5407 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005408 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005409 return 0
5410
5411
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005412class OptionParser(optparse.OptionParser):
5413 """Creates the option parse and add --verbose support."""
5414 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005415 optparse.OptionParser.__init__(
5416 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005417 self.add_option(
5418 '-v', '--verbose', action='count', default=0,
5419 help='Use 2 times for more debugging info')
5420
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005421 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005422 try:
5423 return self._parse_args(args)
5424 finally:
5425 # Regardless of success or failure of args parsing, we want to report
5426 # metrics, but only after logging has been initialized (if parsing
5427 # succeeded).
5428 global settings
5429 settings = Settings()
5430
5431 if not metrics.DISABLE_METRICS_COLLECTION:
5432 # GetViewVCUrl ultimately calls logging method.
5433 project_url = settings.GetViewVCUrl().strip('/+')
5434 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5435 metrics.collector.add('project_urls', [project_url])
5436
5437 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005438 # Create an optparse.Values object that will store only the actual passed
5439 # options, without the defaults.
5440 actual_options = optparse.Values()
5441 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5442 # Create an optparse.Values object with the default options.
5443 options = optparse.Values(self.get_default_values().__dict__)
5444 # Update it with the options passed by the user.
5445 options._update_careful(actual_options.__dict__)
5446 # Store the options passed by the user in an _actual_options attribute.
5447 # We store only the keys, and not the values, since the values can contain
5448 # arbitrary information, which might be PII.
Edward Lesmesf6a22322019-11-04 22:14:39 +00005449 metrics.collector.add('arguments', actual_options.__dict__.keys())
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005450
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005451 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005452 logging.basicConfig(
5453 level=levels[min(options.verbose, len(levels) - 1)],
5454 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5455 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005456
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005457 return options, args
5458
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005459
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005460def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005461 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005462 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005463 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005464 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005465
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005466 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005467 dispatcher = subcommand.CommandDispatcher(__name__)
5468 try:
5469 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00005470 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005471 DieWithError(str(e))
Edward Lesmesf6a22322019-11-04 22:14:39 +00005472 except urllib_error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005473 if e.code != 500:
5474 raise
5475 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005476 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005477 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005478 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005479
5480
5481if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005482 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5483 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005484 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005485 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005486 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005487 sys.exit(main(sys.argv[1:]))