blob: d72410f1486938caff641797170adabd02b72c87 [file] [log] [blame]
Edward Lesmes98eda3f2019-08-12 21:09:53 +00001#!/usr/bin/env python
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 Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000044import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000045import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000046import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000047import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000048import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000049import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000051import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000052import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000053import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
57import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040058import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
Edward Lemur0f58ae42019-04-30 17:24:12 +000065# Traces for git push will be stored in a traces directory inside the
66# depot_tools checkout.
67DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
68TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
69
70# When collecting traces, Git hashes will be reduced to 6 characters to reduce
71# the size after compression.
72GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
73# Used to redact the cookies from the gitcookies file.
74GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
75
Edward Lemur1b52d872019-05-09 21:12:12 +000076# The maximum number of traces we will keep. Multiplied by 3 since we store
77# 3 files per trace.
78MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000079# Message to be displayed to the user to inform where to find the traces for a
80# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000081TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000082'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000083'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000084' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000085'Copies of your gitcookies file and git config have been recorded at:\n'
86' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000087# Format of the message to be stored as part of the traces to give developers a
88# better context when they go through traces.
89TRACES_README_FORMAT = (
90'Date: %(now)s\n'
91'\n'
92'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
93'Title: %(title)s\n'
94'\n'
95'%(description)s\n'
96'\n'
97'Execution time: %(execution_time)s\n'
98'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +000099
tandrii9d2c7a32016-06-22 03:42:45 -0700100COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800101POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000103REFS_THAT_ALIAS_TO_OTHER_REFS = {
104 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
105 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
106}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
thestig@chromium.org44202a22014-03-11 19:22:18 +0000108# Valid extensions for files we want to lint.
109DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
110DEFAULT_LINT_IGNORE_REGEX = r"$^"
111
Aiden Bennerc08566e2018-10-03 17:52:42 +0000112# File name for yapf style config files.
113YAPF_CONFIG_FILENAME = '.style.yapf'
114
borenet6c0efe62016-10-19 08:13:29 -0700115# Buildbucket master name prefix.
116MASTER_PREFIX = 'master.'
117
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000118# Shortcut since it quickly becomes redundant.
119Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000120
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000121# Initialized in main()
122settings = None
123
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100124# Used by tests/git_cl_test.py to add extra logging.
125# Inside the weirdly failing test, add this:
126# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700127# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100128_IS_BEING_TESTED = False
129
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000130
Christopher Lamf732cd52017-01-24 12:40:11 +1100131def DieWithError(message, change_desc=None):
132 if change_desc:
133 SaveDescriptionBackup(change_desc)
134
vapiera7fbd5a2016-06-16 09:17:49 -0700135 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136 sys.exit(1)
137
138
Christopher Lamf732cd52017-01-24 12:40:11 +1100139def SaveDescriptionBackup(change_desc):
140 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000141 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100142 backup_file = open(backup_path, 'w')
143 backup_file.write(change_desc.description)
144 backup_file.close()
145
146
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000147def GetNoGitPagerEnv():
148 env = os.environ.copy()
149 # 'cat' is a magical git string that disables pagers on all platforms.
150 env['GIT_PAGER'] = 'cat'
151 return env
152
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000153
bsep@chromium.org627d9002016-04-29 00:00:52 +0000154def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000156 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000157 except subprocess2.CalledProcessError as e:
158 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000159 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000160 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000161 'Command "%s" failed.\n%s' % (
162 ' '.join(args), error_message or e.stdout or ''))
163 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164
165
166def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000167 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000168 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000169
170
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000171def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000172 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700173 if suppress_stderr:
174 stderr = subprocess2.VOID
175 else:
176 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000177 try:
tandrii5d48c322016-08-18 16:19:37 -0700178 (out, _), code = subprocess2.communicate(['git'] + args,
179 env=GetNoGitPagerEnv(),
180 stdout=subprocess2.PIPE,
181 stderr=stderr)
182 return code, out
183 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900184 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700185 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186
187
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000188def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000189 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000190 return RunGitWithCode(args, suppress_stderr=True)[1]
191
192
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000193def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000194 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000195 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000196 return (version.startswith(prefix) and
197 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000198
199
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000200def BranchExists(branch):
201 """Return True if specified branch exists."""
202 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
203 suppress_stderr=True)
204 return not code
205
206
tandrii2a16b952016-10-19 07:09:44 -0700207def time_sleep(seconds):
208 # Use this so that it can be mocked in tests without interfering with python
209 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700210 return time.sleep(seconds)
211
212
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000213def time_time():
214 # Use this so that it can be mocked in tests without interfering with python
215 # system machinery.
216 return time.time()
217
218
Edward Lemur1b52d872019-05-09 21:12:12 +0000219def datetime_now():
220 # Use this so that it can be mocked in tests without interfering with python
221 # system machinery.
222 return datetime.datetime.now()
223
224
maruel@chromium.org90541732011-04-01 17:54:18 +0000225def ask_for_data(prompt):
226 try:
227 return raw_input(prompt)
228 except KeyboardInterrupt:
229 # Hide the exception.
230 sys.exit(1)
231
232
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100233def confirm_or_exit(prefix='', action='confirm'):
234 """Asks user to press enter to continue or press Ctrl+C to abort."""
235 if not prefix or prefix.endswith('\n'):
236 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100237 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100238 mid = ' Press'
239 elif prefix.endswith(' '):
240 mid = 'press'
241 else:
242 mid = ' press'
243 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
244
245
246def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000247 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100248 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
249 while True:
250 if 'yes'.startswith(result):
251 return True
252 if 'no'.startswith(result):
253 return False
254 result = ask_for_data('Please, type yes or no: ').lower()
255
256
tandrii5d48c322016-08-18 16:19:37 -0700257def _git_branch_config_key(branch, key):
258 """Helper method to return Git config key for a branch."""
259 assert branch, 'branch name is required to set git config for it'
260 return 'branch.%s.%s' % (branch, key)
261
262
263def _git_get_branch_config_value(key, default=None, value_type=str,
264 branch=False):
265 """Returns git config value of given or current branch if any.
266
267 Returns default in all other cases.
268 """
269 assert value_type in (int, str, bool)
270 if branch is False: # Distinguishing default arg value from None.
271 branch = GetCurrentBranch()
272
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000273 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700274 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000275
tandrii5d48c322016-08-18 16:19:37 -0700276 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700277 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700278 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700279 # git config also has --int, but apparently git config suffers from integer
280 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700281 args.append(_git_branch_config_key(branch, key))
282 code, out = RunGitWithCode(args)
283 if code == 0:
284 value = out.strip()
285 if value_type == int:
286 return int(value)
287 if value_type == bool:
288 return bool(value.lower() == 'true')
289 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000290 return default
291
292
tandrii5d48c322016-08-18 16:19:37 -0700293def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000294 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700295
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000296 If value is None, the key will be unset, otherwise it will be set.
297 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700298 """
299 if not branch:
300 branch = GetCurrentBranch()
301 assert branch, 'a branch name OR currently checked out branch is required'
302 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700303 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700304 if value is None:
305 args.append('--unset')
306 elif isinstance(value, bool):
307 args.append('--bool')
308 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700309 else:
tandrii33a46ff2016-08-23 05:53:40 -0700310 # git config also has --int, but apparently git config suffers from integer
311 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700312 value = str(value)
313 args.append(_git_branch_config_key(branch, key))
314 if value is not None:
315 args.append(value)
316 RunGit(args, **kwargs)
317
318
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100319def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700320 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100321
322 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
323 """
324 # Git also stores timezone offset, but it only affects visual display,
325 # actual point in time is defined by this timestamp only.
326 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
327
328
329def _git_amend_head(message, committer_timestamp):
330 """Amends commit with new message and desired committer_timestamp.
331
332 Sets committer timezone to UTC.
333 """
334 env = os.environ.copy()
335 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
336 return RunGit(['commit', '--amend', '-m', message], env=env)
337
338
machenbach@chromium.org45453142015-09-15 08:45:22 +0000339def _get_properties_from_options(options):
340 properties = dict(x.split('=', 1) for x in options.properties)
341 for key, val in properties.iteritems():
342 try:
343 properties[key] = json.loads(val)
344 except ValueError:
345 pass # If a value couldn't be evaluated, treat it as a string.
346 return properties
347
348
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349def _prefix_master(master):
350 """Convert user-specified master name to full master name.
351
352 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
353 name, while the developers always use shortened master name
354 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
355 function does the conversion for buildbucket migration.
356 """
borenet6c0efe62016-10-19 08:13:29 -0700357 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000358 return master
borenet6c0efe62016-10-19 08:13:29 -0700359 return '%s%s' % (MASTER_PREFIX, master)
360
361
362def _unprefix_master(bucket):
363 """Convert bucket name to shortened master name.
364
365 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
366 name, while the developers always use shortened master name
367 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
368 function does the conversion for buildbucket migration.
369 """
370 if bucket.startswith(MASTER_PREFIX):
371 return bucket[len(MASTER_PREFIX):]
372 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
374
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375def _buildbucket_retry(operation_name, http, *args, **kwargs):
376 """Retries requests to buildbucket service and returns parsed json content."""
377 try_count = 0
378 while True:
379 response, content = http.request(*args, **kwargs)
380 try:
381 content_json = json.loads(content)
382 except ValueError:
383 content_json = None
384
385 # Buildbucket could return an error even if status==200.
386 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000387 error = content_json.get('error')
388 if error.get('code') == 403:
389 raise BuildbucketResponseException(
390 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000391 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000392 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000393 raise BuildbucketResponseException(msg)
394
395 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700396 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000397 raise BuildbucketResponseException(
398 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700399 'Please file bugs at http://crbug.com, '
400 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401 content)
402 return content_json
403 if response.status < 500 or try_count >= 2:
404 raise httplib2.HttpLib2Error(content)
405
406 # status >= 500 means transient failures.
407 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700408 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000409 try_count += 1
410 assert False, 'unreachable'
411
412
qyearsley1fdfcb62016-10-24 13:22:03 -0700413def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700414 """Returns a dict mapping bucket names to builders and tests,
415 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700416 """
qyearsleydd49f942016-10-28 11:57:22 -0700417 # If no bots are listed, we try to get a set of builders and tests based
418 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 if not options.bot:
420 change = changelist.GetChange(
421 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700422 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700423 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 change=change,
425 changed_files=change.LocalPaths(),
426 repository_root=settings.GetRoot(),
427 default_presubmit=None,
428 project=None,
429 verbose=options.verbose,
430 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700431 if masters is None:
432 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100433 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700434
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 if options.bucket:
436 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700437 if options.master:
438 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700439
qyearsleydd49f942016-10-28 11:57:22 -0700440 # If bots are listed but no master or bucket, then we need to find out
441 # the corresponding master for each bot.
442 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
443 if error_message:
444 option_parser.error(
445 'Tryserver master cannot be found because: %s\n'
446 'Please manually specify the tryserver master, e.g. '
447 '"-m tryserver.chromium.linux".' % error_message)
448 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700449
450
qyearsley123a4682016-10-26 09:12:17 -0700451def _get_bucket_map_for_builders(builders):
452 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700453 map_url = 'https://builders-map.appspot.com/'
454 try:
qyearsley123a4682016-10-26 09:12:17 -0700455 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700456 except urllib2.URLError as e:
457 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
458 (map_url, e))
459 except ValueError as e:
460 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700461 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, 'Failed to build master map.'
463
qyearsley123a4682016-10-26 09:12:17 -0700464 bucket_map = {}
465 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800466 bucket = builders_map.get(builder, {}).get('bucket')
467 if bucket:
468 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700469 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700470
471
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800472def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700473 """Sends a request to Buildbucket to trigger try jobs for a changelist.
474
475 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700476 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700477 changelist: Changelist that the try jobs are associated with.
478 buckets: A nested dict mapping bucket names to builders to tests.
479 options: Command-line options.
480 """
tandriide281ae2016-10-12 06:02:30 -0700481 assert changelist.GetIssue(), 'CL must be uploaded first'
482 codereview_url = changelist.GetCodereviewServer()
483 assert codereview_url, 'CL must be uploaded first'
484 patchset = patchset or changelist.GetMostRecentPatchset()
485 assert patchset, 'CL must be uploaded first'
486
487 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700488 # Cache the buildbucket credentials under the codereview host key, so that
489 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700490 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 http = authenticator.authorize(httplib2.Http())
492 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700493
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000494 buildbucket_put_url = (
495 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000496 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000497 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700498 hostname=codereview_host,
499 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700501
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700502 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800503 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700504 if options.clobber:
505 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700506 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700507 if extra_properties:
508 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509
510 batch_req_body = {'builds': []}
511 print_text = []
512 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700513 for bucket, builders_and_tests in sorted(buckets.iteritems()):
514 print_text.append('Bucket: %s' % bucket)
515 master = None
516 if bucket.startswith(MASTER_PREFIX):
517 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000518 for builder, tests in sorted(builders_and_tests.iteritems()):
519 print_text.append(' %s: %s' % (builder, tests))
520 parameters = {
521 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100523 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000524 'revision': options.revision,
525 }],
tandrii8c5a3532016-11-04 07:52:02 -0700526 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000527 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000528 if 'presubmit' in builder.lower():
529 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000530 if tests:
531 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700532
533 tags = [
534 'builder:%s' % builder,
535 'buildset:%s' % buildset,
536 'user_agent:git_cl_try',
537 ]
538 if master:
539 parameters['properties']['master'] = master
540 tags.append('master:%s' % master)
541
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000542 batch_req_body['builds'].append(
543 {
544 'bucket': bucket,
545 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700547 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 }
549 )
550
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700552 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http,
554 buildbucket_put_url,
555 'PUT',
556 body=json.dumps(batch_req_body),
557 headers={'Content-Type': 'application/json'}
558 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000559 print_text.append('To see results here, run: git cl try-results')
560 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700561 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000562
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000563
tandrii221ab252016-10-06 08:12:04 -0700564def fetch_try_jobs(auth_config, changelist, buildbucket_host,
565 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700566 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567
qyearsley53f48a12016-09-01 10:45:13 -0700568 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 """
tandrii221ab252016-10-06 08:12:04 -0700570 assert buildbucket_host
571 assert changelist.GetIssue(), 'CL must be uploaded first'
572 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
573 patchset = patchset or changelist.GetMostRecentPatchset()
574 assert patchset, 'CL must be uploaded first'
575
576 codereview_url = changelist.GetCodereviewServer()
577 codereview_host = urlparse.urlparse(codereview_url).hostname
578 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 if authenticator.has_cached_credentials():
580 http = authenticator.authorize(httplib2.Http())
581 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700582 print('Warning: Some results might be missing because %s' %
583 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700584 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 http = httplib2.Http()
586
587 http.force_exception_to_status_code = True
588
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000589 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700590 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700592 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 params = {'tag': 'buildset:%s' % buildset}
594
595 builds = {}
596 while True:
597 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700598 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700600 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 for build in content.get('builds', []):
602 builds[build['id']] = build
603 if 'next_cursor' in content:
604 params['start_cursor'] = content['next_cursor']
605 else:
606 break
607 return builds
608
609
qyearsleyeab3c042016-08-24 09:18:28 -0700610def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611 """Prints nicely result of fetch_try_jobs."""
612 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700613 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614 return
615
616 # Make a copy, because we'll be modifying builds dictionary.
617 builds = builds.copy()
618 builder_names_cache = {}
619
620 def get_builder(b):
621 try:
622 return builder_names_cache[b['id']]
623 except KeyError:
624 try:
625 parameters = json.loads(b['parameters_json'])
626 name = parameters['builder_name']
627 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700628 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700629 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 name = None
631 builder_names_cache[b['id']] = name
632 return name
633
634 def get_bucket(b):
635 bucket = b['bucket']
636 if bucket.startswith('master.'):
637 return bucket[len('master.'):]
638 return bucket
639
640 if options.print_master:
641 name_fmt = '%%-%ds %%-%ds' % (
642 max(len(str(get_bucket(b))) for b in builds.itervalues()),
643 max(len(str(get_builder(b))) for b in builds.itervalues()))
644 def get_name(b):
645 return name_fmt % (get_bucket(b), get_builder(b))
646 else:
647 name_fmt = '%%-%ds' % (
648 max(len(str(get_builder(b))) for b in builds.itervalues()))
649 def get_name(b):
650 return name_fmt % get_builder(b)
651
652 def sort_key(b):
653 return b['status'], b.get('result'), get_name(b), b.get('url')
654
655 def pop(title, f, color=None, **kwargs):
656 """Pop matching builds from `builds` dict and print them."""
657
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000658 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000659 colorize = str
660 else:
661 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
662
663 result = []
664 for b in builds.values():
665 if all(b.get(k) == v for k, v in kwargs.iteritems()):
666 builds.pop(b['id'])
667 result.append(b)
668 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700669 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700671 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673 total = len(builds)
674 pop(status='COMPLETED', result='SUCCESS',
675 title='Successes:', color=Fore.GREEN,
676 f=lambda b: (get_name(b), b.get('url')))
677 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
678 title='Infra Failures:', color=Fore.MAGENTA,
679 f=lambda b: (get_name(b), b.get('url')))
680 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
681 title='Failures:', color=Fore.RED,
682 f=lambda b: (get_name(b), b.get('url')))
683 pop(status='COMPLETED', result='CANCELED',
684 title='Canceled:', color=Fore.MAGENTA,
685 f=lambda b: (get_name(b),))
686 pop(status='COMPLETED', result='FAILURE',
687 failure_reason='INVALID_BUILD_DEFINITION',
688 title='Wrong master/builder name:', color=Fore.MAGENTA,
689 f=lambda b: (get_name(b),))
690 pop(status='COMPLETED', result='FAILURE',
691 title='Other failures:',
692 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
693 pop(status='COMPLETED',
694 title='Other finished:',
695 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
696 pop(status='STARTED',
697 title='Started:', color=Fore.YELLOW,
698 f=lambda b: (get_name(b), b.get('url')))
699 pop(status='SCHEDULED',
700 title='Scheduled:',
701 f=lambda b: (get_name(b), 'id=%s' % b['id']))
702 # The last section is just in case buildbucket API changes OR there is a bug.
703 pop(title='Other:',
704 f=lambda b: (get_name(b), 'id=%s' % b['id']))
705 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700706 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000707
708
Aiden Bennerc08566e2018-10-03 17:52:42 +0000709def _ComputeDiffLineRanges(files, upstream_commit):
710 """Gets the changed line ranges for each file since upstream_commit.
711
712 Parses a git diff on provided files and returns a dict that maps a file name
713 to an ordered list of range tuples in the form (start_line, count).
714 Ranges are in the same format as a git diff.
715 """
716 # If files is empty then diff_output will be a full diff.
717 if len(files) == 0:
718 return {}
719
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000720 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000721 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
722 diff_output = RunGit(diff_cmd)
723
724 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
725 # 2 capture groups
726 # 0 == fname of diff file
727 # 1 == 'diff_start,diff_count' or 'diff_start'
728 # will match each of
729 # diff --git a/foo.foo b/foo.py
730 # @@ -12,2 +14,3 @@
731 # @@ -12,2 +17 @@
732 # running re.findall on the above string with pattern will give
733 # [('foo.py', ''), ('', '14,3'), ('', '17')]
734
735 curr_file = None
736 line_diffs = {}
737 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
738 if match[0] != '':
739 # Will match the second filename in diff --git a/a.py b/b.py.
740 curr_file = match[0]
741 line_diffs[curr_file] = []
742 else:
743 # Matches +14,3
744 if ',' in match[1]:
745 diff_start, diff_count = match[1].split(',')
746 else:
747 # Single line changes are of the form +12 instead of +12,1.
748 diff_start = match[1]
749 diff_count = 1
750
751 diff_start = int(diff_start)
752 diff_count = int(diff_count)
753
754 # If diff_count == 0 this is a removal we can ignore.
755 line_diffs[curr_file].append((diff_start, diff_count))
756
757 return line_diffs
758
759
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000760def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000761 """Checks if a yapf file is in any parent directory of fpath until top_dir.
762
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000763 Recursively checks parent directories to find yapf file and if no yapf file
764 is found returns None. Uses yapf_config_cache as a cache for
765 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000766 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000767 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000768 # Return result if we've already computed it.
769 if fpath in yapf_config_cache:
770 return yapf_config_cache[fpath]
771
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000772 parent_dir = os.path.dirname(fpath)
773 if os.path.isfile(fpath):
774 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000775 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000776 # Otherwise fpath is a directory
777 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
778 if os.path.isfile(yapf_file):
779 ret = yapf_file
780 elif fpath == top_dir or parent_dir == fpath:
781 # If we're at the top level directory, or if we're at root
782 # there is no provided style.
783 ret = None
784 else:
785 # Otherwise recurse on the current directory.
786 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000787 yapf_config_cache[fpath] = ret
788 return ret
789
790
qyearsley53f48a12016-09-01 10:45:13 -0700791def write_try_results_json(output_file, builds):
792 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
793
794 The input |builds| dict is assumed to be generated by Buildbucket.
795 Buildbucket documentation: http://goo.gl/G0s101
796 """
797
798 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800799 """Extracts some of the information from one build dict."""
800 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700801 return {
802 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700803 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800804 'builder_name': parameters.get('builder_name'),
805 'created_ts': build.get('created_ts'),
806 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700807 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800808 'result': build.get('result'),
809 'status': build.get('status'),
810 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700811 'url': build.get('url'),
812 }
813
814 converted = []
815 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000816 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700817 write_json(output_file, converted)
818
819
Aaron Gable13101a62018-02-09 13:20:41 -0800820def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000821 """Prints statistics about the change to the user."""
822 # --no-ext-diff is broken in some versions of Git, so try to work around
823 # this by overriding the environment (but there is still a problem if the
824 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000825 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000826 if 'GIT_EXTERNAL_DIFF' in env:
827 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000828
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000829 try:
830 stdout = sys.stdout.fileno()
831 except AttributeError:
832 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000833 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800834 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000835 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000836
837
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000838class BuildbucketResponseException(Exception):
839 pass
840
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842class Settings(object):
843 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000845 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 self.tree_status_url = None
847 self.viewvc_url = None
848 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000849 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000850 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000851 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000852 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
854 def LazyUpdateIfNeeded(self):
855 """Updates the settings from a codereview.settings file, if available."""
856 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000857 # The only value that actually changes the behavior is
858 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000859 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000860 error_ok=True
861 ).strip().lower()
862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000864 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 LoadCodereviewSettingsFromFile(cr_settings_file)
866 self.updated = True
867
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000868 @staticmethod
869 def GetRelativeRoot():
870 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000873 if self.root is None:
874 self.root = os.path.abspath(self.GetRelativeRoot())
875 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 def GetTreeStatusUrl(self, error_ok=False):
878 if not self.tree_status_url:
879 error_message = ('You must configure your tree status URL by running '
880 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000881 self.tree_status_url = self._GetConfig(
882 'rietveld.tree-status-url', error_ok=error_ok,
883 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 return self.tree_status_url
885
886 def GetViewVCUrl(self):
887 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000888 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 return self.viewvc_url
890
rmistry@google.com90752582014-01-14 21:04:50 +0000891 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000892 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000893
rmistry@google.com5626a922015-02-26 14:03:30 +0000894 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000895 run_post_upload_hook = self._GetConfig(
896 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000897 return run_post_upload_hook == "True"
898
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000899 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000900 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000901
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700903 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700905 self.is_gerrit = (
906 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000907 return self.is_gerrit
908
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000909 def GetSquashGerritUploads(self):
910 """Return true if uploads to Gerrit should be squashed by default."""
911 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700912 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
913 if self.squash_gerrit_uploads is None:
914 # Default is squash now (http://crbug.com/611892#c23).
915 self.squash_gerrit_uploads = not (
916 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
917 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000918 return self.squash_gerrit_uploads
919
tandriia60502f2016-06-20 02:01:53 -0700920 def GetSquashGerritUploadsOverride(self):
921 """Return True or False if codereview.settings should be overridden.
922
923 Returns None if no override has been defined.
924 """
925 # See also http://crbug.com/611892#c23
926 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
927 error_ok=True).strip()
928 if result == 'true':
929 return True
930 if result == 'false':
931 return False
932 return None
933
tandrii@chromium.org28253532016-04-14 13:46:56 +0000934 def GetGerritSkipEnsureAuthenticated(self):
935 """Return True if EnsureAuthenticated should not be done for Gerrit
936 uploads."""
937 if self.gerrit_skip_ensure_authenticated is None:
938 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000939 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000940 error_ok=True).strip() == 'true')
941 return self.gerrit_skip_ensure_authenticated
942
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000943 def GetGitEditor(self):
944 """Return the editor specified in the git config, or None if none is."""
945 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000946 # Git requires single quotes for paths with spaces. We need to replace
947 # them with double quotes for Windows to treat such paths as a single
948 # path.
949 self.git_editor = self._GetConfig(
950 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000951 return self.git_editor or None
952
thestig@chromium.org44202a22014-03-11 19:22:18 +0000953 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000954 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000955 DEFAULT_LINT_REGEX)
956
957 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000958 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000959 DEFAULT_LINT_IGNORE_REGEX)
960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 def _GetConfig(self, param, **kwargs):
962 self.LazyUpdateIfNeeded()
963 return RunGit(['config', param], **kwargs).strip()
964
965
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100966@contextlib.contextmanager
967def _get_gerrit_project_config_file(remote_url):
968 """Context manager to fetch and store Gerrit's project.config from
969 refs/meta/config branch and store it in temp file.
970
971 Provides a temporary filename or None if there was error.
972 """
973 error, _ = RunGitWithCode([
974 'fetch', remote_url,
975 '+refs/meta/config:refs/git_cl/meta/config'])
976 if error:
977 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700978 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100979 (remote_url, error))
980 yield None
981 return
982
983 error, project_config_data = RunGitWithCode(
984 ['show', 'refs/git_cl/meta/config:project.config'])
985 if error:
986 print('WARNING: project.config file not found')
987 yield None
988 return
989
990 with gclient_utils.temporary_directory() as tempdir:
991 project_config_file = os.path.join(tempdir, 'project.config')
992 gclient_utils.FileWrite(project_config_file, project_config_data)
993 yield project_config_file
994
995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996def ShortBranchName(branch):
997 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000998 return branch.replace('refs/heads/', '', 1)
999
1000
1001def GetCurrentBranchRef():
1002 """Returns branch ref (e.g., refs/heads/master) or None."""
1003 return RunGit(['symbolic-ref', 'HEAD'],
1004 stderr=subprocess2.VOID, error_ok=True).strip() or None
1005
1006
1007def GetCurrentBranch():
1008 """Returns current branch or None.
1009
1010 For refs/heads/* branches, returns just last part. For others, full ref.
1011 """
1012 branchref = GetCurrentBranchRef()
1013 if branchref:
1014 return ShortBranchName(branchref)
1015 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016
1017
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001018class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001019 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001020 NONE = 'none'
1021 DRY_RUN = 'dry_run'
1022 COMMIT = 'commit'
1023
1024 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1025
1026
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001027class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001028 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001029 self.issue = issue
1030 self.patchset = patchset
1031 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001032 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001033 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001034
1035 @property
1036 def valid(self):
1037 return self.issue is not None
1038
1039
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001040def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1042 fail_result = _ParsedIssueNumberArgument()
1043
1044 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001045 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001046 if not arg.startswith('http'):
1047 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001048
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001049 url = gclient_utils.UpgradeToHttps(arg)
1050 try:
1051 parsed_url = urlparse.urlparse(url)
1052 except ValueError:
1053 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001054
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001055 if codereview is not None:
1056 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1057 return parsed or fail_result
1058
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001059 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060
1061
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001062def _create_description_from_log(args):
1063 """Pulls out the commit log to use as a base for the CL description."""
1064 log_args = []
1065 if len(args) == 1 and not args[0].endswith('.'):
1066 log_args = [args[0] + '..']
1067 elif len(args) == 1 and args[0].endswith('...'):
1068 log_args = [args[0][:-1]]
1069 elif len(args) == 2:
1070 log_args = [args[0] + '..' + args[1]]
1071 else:
1072 log_args = args[:] # Hope for the best!
1073 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1074
1075
Aaron Gablea45ee112016-11-22 15:14:38 -08001076class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001077 def __init__(self, issue, url):
1078 self.issue = issue
1079 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001080 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001081
1082 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001083 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001084 self.issue, self.url)
1085
1086
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001087_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001088 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001089 # TODO(tandrii): these two aren't known in Gerrit.
1090 'approval', 'disapproval'])
1091
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001094 """Changelist works with one changelist in local branch.
1095
1096 Supports two codereview backends: Rietveld or Gerrit, selected at object
1097 creation.
1098
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001099 Notes:
1100 * Not safe for concurrent multi-{thread,process} use.
1101 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001102 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001103 """
1104
1105 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1106 """Create a new ChangeList instance.
1107
1108 If issue is given, the codereview must be given too.
1109
1110 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1111 Otherwise, it's decided based on current configuration of the local branch,
1112 with default being 'rietveld' for backwards compatibility.
1113 See _load_codereview_impl for more details.
1114
1115 **kwargs will be passed directly to codereview implementation.
1116 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001118 global settings
1119 if not settings:
1120 # Happens when git_cl.py is used as a utility library.
1121 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001122
1123 if issue:
1124 assert codereview, 'codereview must be known, if issue is known'
1125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 self.branchref = branchref
1127 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001128 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 self.branch = ShortBranchName(self.branchref)
1130 else:
1131 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001133 self.lookedup_issue = False
1134 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 self.has_description = False
1136 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001137 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001139 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001140 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001141 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001142 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001143
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001146 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001147 assert self._codereview_impl
1148 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149
1150 def _load_codereview_impl(self, codereview=None, **kwargs):
1151 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001152 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1153 'codereview {} not in {}'.format(codereview,
1154 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1156 self._codereview = codereview
1157 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001158 return
1159
1160 # Automatic selection based on issue number set for a current branch.
1161 # Rietveld takes precedence over Gerrit.
1162 assert not self.issue
1163 # Whether we find issue or not, we are doing the lookup.
1164 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001165 if self.GetBranch():
1166 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1167 issue = _git_get_branch_config_value(
1168 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1169 if issue:
1170 self._codereview = codereview
1171 self._codereview_impl = cls(self, **kwargs)
1172 self.issue = int(issue)
1173 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174
Bryce Thomascfc97122018-12-13 20:21:47 +00001175 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001177 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 **kwargs)
1179
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001180 def IsGerrit(self):
1181 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001182
1183 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001184 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001185
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001186 The return value is a string suitable for passing to git cl with the --cc
1187 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001188 """
1189 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001190 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001191 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1193 return self.cc
1194
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001195 def GetCCListWithoutDefault(self):
1196 """Return the users cc'd on this CL excluding default ones."""
1197 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001198 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001199 return self.cc
1200
Daniel Cheng7227d212017-11-17 08:12:37 -08001201 def ExtendCC(self, more_cc):
1202 """Extends the list of users to cc on this CL based on the changed files."""
1203 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
1205 def GetBranch(self):
1206 """Returns the short branch name, e.g. 'master'."""
1207 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001208 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001209 if not branchref:
1210 return None
1211 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 self.branch = ShortBranchName(self.branchref)
1213 return self.branch
1214
1215 def GetBranchRef(self):
1216 """Returns the full branch name, e.g. 'refs/heads/master'."""
1217 self.GetBranch() # Poke the lazy loader.
1218 return self.branchref
1219
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001220 def ClearBranch(self):
1221 """Clears cached branch data of this object."""
1222 self.branch = self.branchref = None
1223
tandrii5d48c322016-08-18 16:19:37 -07001224 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1225 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1226 kwargs['branch'] = self.GetBranch()
1227 return _git_get_branch_config_value(key, default, **kwargs)
1228
1229 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1230 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1231 assert self.GetBranch(), (
1232 'this CL must have an associated branch to %sset %s%s' %
1233 ('un' if value is None else '',
1234 key,
1235 '' if value is None else ' to %r' % value))
1236 kwargs['branch'] = self.GetBranch()
1237 return _git_set_branch_config_value(key, value, **kwargs)
1238
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001239 @staticmethod
1240 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001241 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 e.g. 'origin', 'refs/heads/master'
1243 """
1244 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001245 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001248 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001250 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1251 error_ok=True).strip()
1252 if upstream_branch:
1253 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001255 # Else, try to guess the origin remote.
1256 remote_branches = RunGit(['branch', '-r']).split()
1257 if 'origin/master' in remote_branches:
1258 # Fall back on origin/master if it exits.
1259 remote = 'origin'
1260 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001262 DieWithError(
1263 'Unable to determine default branch to diff against.\n'
1264 'Either pass complete "git diff"-style arguments, like\n'
1265 ' git cl upload origin/master\n'
1266 'or verify this branch is set up to track another \n'
1267 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268
1269 return remote, upstream_branch
1270
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001272 upstream_branch = self.GetUpstreamBranch()
1273 if not BranchExists(upstream_branch):
1274 DieWithError('The upstream for the current branch (%s) does not exist '
1275 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001276 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001277 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 def GetUpstreamBranch(self):
1280 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001282 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001283 upstream_branch = upstream_branch.replace('refs/heads/',
1284 'refs/remotes/%s/' % remote)
1285 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1286 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 self.upstream_branch = upstream_branch
1288 return self.upstream_branch
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001291 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 remote, branch = None, self.GetBranch()
1293 seen_branches = set()
1294 while branch not in seen_branches:
1295 seen_branches.add(branch)
1296 remote, branch = self.FetchUpstreamTuple(branch)
1297 branch = ShortBranchName(branch)
1298 if remote != '.' or branch.startswith('refs/remotes'):
1299 break
1300 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 remotes = RunGit(['remote'], error_ok=True).split()
1302 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001304 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001306 logging.warn('Could not determine which remote this change is '
1307 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001308 else:
1309 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001310 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 branch = 'HEAD'
1312 if branch.startswith('refs/remotes'):
1313 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001314 elif branch.startswith('refs/branch-heads/'):
1315 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 else:
1317 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001318 return self._remote
1319
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 def GitSanityChecks(self, upstream_git_obj):
1321 """Checks git repo status and ensures diff is from local commits."""
1322
sbc@chromium.org79706062015-01-14 21:18:12 +00001323 if upstream_git_obj is None:
1324 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001325 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001326 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001327 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001328 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001329 return False
1330
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 # Verify the commit we're diffing against is in our current branch.
1332 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1333 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1334 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001335 print('ERROR: %s is not in the current branch. You may need to rebase '
1336 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 return False
1338
1339 # List the commits inside the diff, and verify they are all local.
1340 commits_in_diff = RunGit(
1341 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1342 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1343 remote_branch = remote_branch.strip()
1344 if code != 0:
1345 _, remote_branch = self.GetRemoteBranch()
1346
1347 commits_in_remote = RunGit(
1348 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1349
1350 common_commits = set(commits_in_diff) & set(commits_in_remote)
1351 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001352 print('ERROR: Your diff contains %d commits already in %s.\n'
1353 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1354 'the diff. If you are using a custom git flow, you can override'
1355 ' the reference used for this check with "git config '
1356 'gitcl.remotebranch <git-ref>".' % (
1357 len(common_commits), remote_branch, upstream_git_obj),
1358 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001359 return False
1360 return True
1361
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001362 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001363 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001364
1365 Returns None if it is not set.
1366 """
tandrii5d48c322016-08-18 16:19:37 -07001367 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 def GetRemoteUrl(self):
1370 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1371
1372 Returns None if there is no remote.
1373 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001374 is_cached, value = self._cached_remote_url
1375 if is_cached:
1376 return value
1377
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001378 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001379 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1380
Edward Lemur298f2cf2019-02-22 21:40:39 +00001381 # Check if the remote url can be parsed as an URL.
1382 host = urlparse.urlparse(url).netloc
1383 if host:
1384 self._cached_remote_url = (True, url)
1385 return url
1386
1387 # If it cannot be parsed as an url, assume it is a local directory, probably
1388 # a git cache.
1389 logging.warning('"%s" doesn\'t appear to point to a git host. '
1390 'Interpreting it as a local directory.', url)
1391 if not os.path.isdir(url):
1392 logging.error(
1393 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1394 remote, url, self.GetBranch())
1395 return None
1396
1397 cache_path = url
1398 url = RunGit(['config', 'remote.%s.url' % remote],
1399 error_ok=True,
1400 cwd=url).strip()
1401
1402 host = urlparse.urlparse(url).netloc
1403 if not host:
1404 logging.error(
1405 'Remote "%(remote)s" for branch "%(branch)s" points to '
1406 '"%(cache_path)s", but it is misconfigured.\n'
1407 '"%(cache_path)s" must be a git repo and must have a remote named '
1408 '"%(remote)s" pointing to the git host.', {
1409 'remote': remote,
1410 'cache_path': cache_path,
1411 'branch': self.GetBranch()})
1412 return None
1413
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001414 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001415 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001417 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001418 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001419 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001420 self.issue = self._GitGetBranchConfigValue(
1421 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001422 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return self.issue
1424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 def GetIssueURL(self):
1426 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001427 issue = self.GetIssue()
1428 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001429 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001430 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001432 def GetDescription(self, pretty=False, force=False):
1433 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001435 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 self.has_description = True
1437 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001438 # Set width to 72 columns + 2 space indent.
1439 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001441 lines = self.description.splitlines()
1442 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.description
1444
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001445 def GetDescriptionFooters(self):
1446 """Returns (non_footer_lines, footers) for the commit message.
1447
1448 Returns:
1449 non_footer_lines (list(str)) - Simple list of description lines without
1450 any footer. The lines do not contain newlines, nor does the list contain
1451 the empty line between the message and the footers.
1452 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1453 [("Change-Id", "Ideadbeef...."), ...]
1454 """
1455 raw_description = self.GetDescription()
1456 msg_lines, _, footers = git_footers.split_footers(raw_description)
1457 if footers:
1458 msg_lines = msg_lines[:len(msg_lines)-1]
1459 return msg_lines, footers
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001462 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001464 self.patchset = self._GitGetBranchConfigValue(
1465 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001466 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 return self.patchset
1468
1469 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001470 """Set this branch's patchset. If patchset=0, clears the patchset."""
1471 assert self.GetBranch()
1472 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001473 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001474 else:
1475 self.patchset = int(patchset)
1476 self._GitSetBranchConfigValue(
1477 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001479 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001480 """Set this branch's issue. If issue isn't given, clears the issue."""
1481 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001483 issue = int(issue)
1484 self._GitSetBranchConfigValue(
1485 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001486 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001487 codereview_server = self._codereview_impl.GetCodereviewServer()
1488 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001489 self._GitSetBranchConfigValue(
1490 self._codereview_impl.CodereviewServerConfigKey(),
1491 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 else:
tandrii5d48c322016-08-18 16:19:37 -07001493 # Reset all of these just to be clean.
1494 reset_suffixes = [
1495 'last-upload-hash',
1496 self._codereview_impl.IssueConfigKey(),
1497 self._codereview_impl.PatchsetConfigKey(),
1498 self._codereview_impl.CodereviewServerConfigKey(),
1499 ] + self._PostUnsetIssueProperties()
1500 for prop in reset_suffixes:
1501 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001502 msg = RunGit(['log', '-1', '--format=%B']).strip()
1503 if msg and git_footers.get_footer_change_id(msg):
1504 print('WARNING: The change patched into this branch has a Change-Id. '
1505 'Removing it.')
1506 RunGit(['commit', '--amend', '-m',
1507 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001508 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001509 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510
dnjba1b0f32016-09-02 12:37:42 -07001511 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001512 if not self.GitSanityChecks(upstream_branch):
1513 DieWithError('\nGit sanity check failure')
1514
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001515 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001516 if not root:
1517 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001518 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519
1520 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001521 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001522 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001523 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001524 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001525 except subprocess2.CalledProcessError:
1526 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001527 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001528 'This branch probably doesn\'t exist anymore. To reset the\n'
1529 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001530 ' git branch --set-upstream-to origin/master %s\n'
1531 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001532 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001533
maruel@chromium.org52424302012-08-29 15:14:30 +00001534 issue = self.GetIssue()
1535 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001536 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001537 description = self.GetDescription()
1538 else:
1539 # If the change was never uploaded, use the log messages of all commits
1540 # up to the branch point, as git cl upload will prefill the description
1541 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001542 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1543 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001544
1545 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001546 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001547 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001548 name,
1549 description,
1550 absroot,
1551 files,
1552 issue,
1553 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001554 author,
1555 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001556
dsansomee2d6fd92016-09-08 00:10:47 -07001557 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001558 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001559 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001560 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001562 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1563 """Sets the description for this CL remotely.
1564
1565 You can get description_lines and footers with GetDescriptionFooters.
1566
1567 Args:
1568 description_lines (list(str)) - List of CL description lines without
1569 newline characters.
1570 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1571 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1572 `List-Of-Tokens`). It will be case-normalized so that each token is
1573 title-cased.
1574 """
1575 new_description = '\n'.join(description_lines)
1576 if footers:
1577 new_description += '\n'
1578 for k, v in footers:
1579 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1580 if not git_footers.FOOTER_PATTERN.match(foot):
1581 raise ValueError('Invalid footer %r' % foot)
1582 new_description += foot + '\n'
1583 self.UpdateDescription(new_description, force)
1584
Edward Lesmes8e282792018-04-03 18:50:29 -04001585 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001586 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1587 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001588 start = time_time()
1589 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1591 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001592 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1593 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001594 metrics.collector.add_repeated('sub_commands', {
1595 'command': 'presubmit',
1596 'execution_time': time_time() - start,
1597 'exit_code': 0 if result.should_continue() else 1,
1598 })
1599 return result
vapierfd77ac72016-06-16 08:33:57 -07001600 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001601 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001602
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001603 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1604 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001605 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1606 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001607 else:
1608 # Assume url.
1609 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1610 urlparse.urlparse(issue_arg))
1611 if not parsed_issue_arg or not parsed_issue_arg.valid:
1612 DieWithError('Failed to parse issue argument "%s". '
1613 'Must be an issue number or a valid URL.' % issue_arg)
1614 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001615 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001616
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617 def CMDUpload(self, options, git_diff_args, orig_args):
1618 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001619 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001620 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001622 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 else:
1624 if self.GetBranch() is None:
1625 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1626
1627 # Default to diffing against common ancestor of upstream branch
1628 base_branch = self.GetCommonAncestorWithUpstream()
1629 git_diff_args = [base_branch, 'HEAD']
1630
Aaron Gablec4c40d12017-05-22 11:49:53 -07001631
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001632 # Fast best-effort checks to abort before running potentially
1633 # expensive hooks if uploading is likely to fail anyway. Passing these
1634 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001635 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001636 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637
1638 # Apply watchlists on upload.
1639 change = self.GetChange(base_branch, None)
1640 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1641 files = [f.LocalPath() for f in change.AffectedFiles()]
1642 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001643 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644
1645 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001646 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001647 # Set the reviewer list now so that presubmit checks can access it.
1648 change_description = ChangeDescription(change.FullDescriptionText())
1649 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001650 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001651 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 change)
1653 change.SetDescriptionText(change_description.description)
1654 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001655 may_prompt=not options.force,
1656 verbose=options.verbose,
1657 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001658 if not hook_results.should_continue():
1659 return 1
1660 if not options.reviewers and hook_results.reviewers:
1661 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001662 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001663
Aaron Gable13101a62018-02-09 13:20:41 -08001664 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001665 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001666 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001667 _git_set_branch_config_value('last-upload-hash',
1668 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 # Run post upload hooks, if specified.
1670 if settings.GetRunPostUploadHook():
1671 presubmit_support.DoPostUploadExecuter(
1672 change,
1673 self,
1674 settings.GetRoot(),
1675 options.verbose,
1676 sys.stdout)
1677
1678 # Upload all dependencies if specified.
1679 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001680 print()
1681 print('--dependencies has been specified.')
1682 print('All dependent local branches will be re-uploaded.')
1683 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001684 # Remove the dependencies flag from args so that we do not end up in a
1685 # loop.
1686 orig_args.remove('--dependencies')
1687 ret = upload_branch_deps(self, orig_args)
1688 return ret
1689
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001690 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001691 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001692
1693 Issue must have been already uploaded and known.
1694 """
1695 assert new_state in _CQState.ALL_STATES
1696 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001697 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001698 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001699 return 0
1700 except KeyboardInterrupt:
1701 raise
1702 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001703 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001704 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001705 ' * Your project has no CQ,\n'
1706 ' * You don\'t have permission to change the CQ state,\n'
1707 ' * There\'s a bug in this code (see stack trace below).\n'
1708 'Consider specifying which bots to trigger manually or asking your '
1709 'project owners for permissions or contacting Chrome Infra at:\n'
1710 'https://www.chromium.org/infra\n\n' %
1711 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001712 # Still raise exception so that stack trace is printed.
1713 raise
1714
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001715 # Forward methods to codereview specific implementation.
1716
Aaron Gable636b13f2017-07-14 10:42:48 -07001717 def AddComment(self, message, publish=None):
1718 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001719
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001720 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001721 """Returns list of _CommentSummary for each comment.
1722
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001723 args:
1724 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001725 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001726 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001727
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001728 def CloseIssue(self):
1729 return self._codereview_impl.CloseIssue()
1730
1731 def GetStatus(self):
1732 return self._codereview_impl.GetStatus()
1733
1734 def GetCodereviewServer(self):
1735 return self._codereview_impl.GetCodereviewServer()
1736
tandriide281ae2016-10-12 06:02:30 -07001737 def GetIssueOwner(self):
1738 """Get owner from codereview, which may differ from this checkout."""
1739 return self._codereview_impl.GetIssueOwner()
1740
Edward Lemur707d70b2018-02-07 00:50:14 +01001741 def GetReviewers(self):
1742 return self._codereview_impl.GetReviewers()
1743
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744 def GetMostRecentPatchset(self):
1745 return self._codereview_impl.GetMostRecentPatchset()
1746
tandriide281ae2016-10-12 06:02:30 -07001747 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001748 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001749 return self._codereview_impl.CannotTriggerTryJobReason()
1750
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001751 def GetTryJobProperties(self, patchset=None):
1752 """Returns dictionary of properties to launch try job."""
1753 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001754
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 def __getattr__(self, attr):
1756 # This is because lots of untested code accesses Rietveld-specific stuff
1757 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001758 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001759 # Note that child method defines __getattr__ as well, and forwards it here,
1760 # because _RietveldChangelistImpl is not cleaned up yet, and given
1761 # deprecation of Rietveld, it should probably be just removed.
1762 # Until that time, avoid infinite recursion by bypassing __getattr__
1763 # of implementation class.
1764 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765
1766
1767class _ChangelistCodereviewBase(object):
1768 """Abstract base class encapsulating codereview specifics of a changelist."""
1769 def __init__(self, changelist):
1770 self._changelist = changelist # instance of Changelist
1771
1772 def __getattr__(self, attr):
1773 # Forward methods to changelist.
1774 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1775 # _RietveldChangelistImpl to avoid this hack?
1776 return getattr(self._changelist, attr)
1777
1778 def GetStatus(self):
1779 """Apply a rough heuristic to give a simple summary of an issue's review
1780 or CQ status, assuming adherence to a common workflow.
1781
1782 Returns None if no issue for this branch, or specific string keywords.
1783 """
1784 raise NotImplementedError()
1785
1786 def GetCodereviewServer(self):
1787 """Returns server URL without end slash, like "https://codereview.com"."""
1788 raise NotImplementedError()
1789
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001790 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 """Fetches and returns description from the codereview server."""
1792 raise NotImplementedError()
1793
tandrii5d48c322016-08-18 16:19:37 -07001794 @classmethod
1795 def IssueConfigKey(cls):
1796 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 raise NotImplementedError()
1798
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001799 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001800 def PatchsetConfigKey(cls):
1801 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 raise NotImplementedError()
1803
tandrii5d48c322016-08-18 16:19:37 -07001804 @classmethod
1805 def CodereviewServerConfigKey(cls):
1806 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 raise NotImplementedError()
1808
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001809 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001810 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001811 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001812
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001813 def GetGerritObjForPresubmit(self):
1814 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1815 return None
1816
dsansomee2d6fd92016-09-08 00:10:47 -07001817 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 """Update the description on codereview site."""
1819 raise NotImplementedError()
1820
Aaron Gable636b13f2017-07-14 10:42:48 -07001821 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001822 """Posts a comment to the codereview site."""
1823 raise NotImplementedError()
1824
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001825 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def CloseIssue(self):
1829 """Closes the issue."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 def GetMostRecentPatchset(self):
1833 """Returns the most recent patchset number from the codereview site."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001836 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001837 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001838 """Fetches and applies the issue.
1839
1840 Arguments:
1841 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1842 reject: if True, reject the failed patch instead of switching to 3-way
1843 merge. Rietveld only.
1844 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1845 only.
1846 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001847 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001848 """
1849 raise NotImplementedError()
1850
1851 @staticmethod
1852 def ParseIssueURL(parsed_url):
1853 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1854 failed."""
1855 raise NotImplementedError()
1856
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001858 """Best effort check that user is authenticated with codereview server.
1859
1860 Arguments:
1861 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001862 refresh: whether to attempt to refresh credentials. Ignored if not
1863 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001865 raise NotImplementedError()
1866
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001867 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001868 """Best effort check that uploading isn't supposed to fail for predictable
1869 reasons.
1870
1871 This method should raise informative exception if uploading shouldn't
1872 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001873
1874 Arguments:
1875 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001877 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001878
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001879 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001880 """Uploads a change to codereview."""
1881 raise NotImplementedError()
1882
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001883 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001884 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001885
1886 Issue must have been already uploaded and known.
1887 """
1888 raise NotImplementedError()
1889
tandriie113dfd2016-10-11 10:20:12 -07001890 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001891 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001892 raise NotImplementedError()
1893
tandriide281ae2016-10-12 06:02:30 -07001894 def GetIssueOwner(self):
1895 raise NotImplementedError()
1896
Edward Lemur707d70b2018-02-07 00:50:14 +01001897 def GetReviewers(self):
1898 raise NotImplementedError()
1899
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001900 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001901 raise NotImplementedError()
1902
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001905 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906 # auth_config is Rietveld thing, kept here to preserve interface only.
1907 super(_GerritChangelistImpl, self).__init__(changelist)
1908 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001909 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001910 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001911 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001912 # Map from change number (issue) to its detail cache.
1913 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001915 if codereview_host is not None:
1916 assert not codereview_host.startswith('https://'), codereview_host
1917 self._gerrit_host = codereview_host
1918 self._gerrit_server = 'https://%s' % codereview_host
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def _GetGerritHost(self):
1921 # Lazy load of configs.
1922 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001923 if self._gerrit_host and '.' not in self._gerrit_host:
1924 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1925 # This happens for internal stuff http://crbug.com/614312.
1926 parsed = urlparse.urlparse(self.GetRemoteUrl())
1927 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001928 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001929 ' Your current remote is: %s' % self.GetRemoteUrl())
1930 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1931 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 return self._gerrit_host
1933
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001934 def _GetGitHost(self):
1935 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001936 remote_url = self.GetRemoteUrl()
1937 if not remote_url:
1938 return None
1939 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001940
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 def GetCodereviewServer(self):
1942 if not self._gerrit_server:
1943 # If we're on a branch then get the server potentially associated
1944 # with that branch.
1945 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001946 self._gerrit_server = self._GitGetBranchConfigValue(
1947 self.CodereviewServerConfigKey())
1948 if self._gerrit_server:
1949 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001950 if not self._gerrit_server:
1951 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1952 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001953 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 parts[0] = parts[0] + '-review'
1955 self._gerrit_host = '.'.join(parts)
1956 self._gerrit_server = 'https://%s' % self._gerrit_host
1957 return self._gerrit_server
1958
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001959 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001960 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001961 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001962 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001963 logging.warn('can\'t detect Gerrit project.')
1964 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001965 project = urlparse.urlparse(remote_url).path.strip('/')
1966 if project.endswith('.git'):
1967 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001968 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1969 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1970 # gitiles/git-over-https protocol. E.g.,
1971 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1972 # as
1973 # https://chromium.googlesource.com/v8/v8
1974 if project.startswith('a/'):
1975 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001976 return project
1977
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001978 def _GerritChangeIdentifier(self):
1979 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1980
1981 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001982 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001983 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001984 project = self._GetGerritProject()
1985 if project:
1986 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1987 # Fall back on still unique, but less efficient change number.
1988 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001989
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001990 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001991 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001992 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001993
tandrii5d48c322016-08-18 16:19:37 -07001994 @classmethod
1995 def PatchsetConfigKey(cls):
1996 return 'gerritpatchset'
1997
1998 @classmethod
1999 def CodereviewServerConfigKey(cls):
2000 return 'gerritserver'
2001
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002002 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002003 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002004 if settings.GetGerritSkipEnsureAuthenticated():
2005 # For projects with unusual authentication schemes.
2006 # See http://crbug.com/603378.
2007 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002008
2009 # Check presence of cookies only if using cookies-based auth method.
2010 cookie_auth = gerrit_util.Authenticator.get()
2011 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002012 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002013
Daniel Chengcf6269b2019-05-18 01:02:12 +00002014 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
2015 print('WARNING: Ignoring branch %s with non-https remote %s' %
2016 (self._changelist.branch, self.GetRemoteUrl()))
2017 return
2018
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002019 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002020 self.GetCodereviewServer()
2021 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002022 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023
2024 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2025 git_auth = cookie_auth.get_auth_header(git_host)
2026 if gerrit_auth and git_auth:
2027 if gerrit_auth == git_auth:
2028 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002029 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002030 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002031 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002032 ' %s\n'
2033 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002034 ' Consider running the following command:\n'
2035 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002036 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002037 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002038 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002039 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002040 cookie_auth.get_new_password_message(git_host)))
2041 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002042 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002043 return
2044 else:
2045 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002046 ([] if gerrit_auth else [self._gerrit_host]) +
2047 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002048 DieWithError('Credentials for the following hosts are required:\n'
2049 ' %s\n'
2050 'These are read from %s (or legacy %s)\n'
2051 '%s' % (
2052 '\n '.join(missing),
2053 cookie_auth.get_gitcookies_path(),
2054 cookie_auth.get_netrc_path(),
2055 cookie_auth.get_new_password_message(git_host)))
2056
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002057 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002058 if not self.GetIssue():
2059 return
2060
2061 # Warm change details cache now to avoid RPCs later, reducing latency for
2062 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002063 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002064 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002065
2066 status = self._GetChangeDetail()['status']
2067 if status in ('MERGED', 'ABANDONED'):
2068 DieWithError('Change %s has been %s, new uploads are not allowed' %
2069 (self.GetIssueURL(),
2070 'submitted' if status == 'MERGED' else 'abandoned'))
2071
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002072 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2073 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2074 # Apparently this check is not very important? Otherwise get_auth_email
2075 # could have been added to other implementations of Authenticator.
2076 cookies_auth = gerrit_util.Authenticator.get()
2077 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002078 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002079
2080 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002081 if self.GetIssueOwner() == cookies_user:
2082 return
2083 logging.debug('change %s owner is %s, cookies user is %s',
2084 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002085 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002086 # so ask what Gerrit thinks of this user.
2087 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2088 if details['email'] == self.GetIssueOwner():
2089 return
2090 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002091 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002092 'as %s.\n'
2093 'Uploading may fail due to lack of permissions.' %
2094 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2095 confirm_or_exit(action='upload')
2096
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002097 def _PostUnsetIssueProperties(self):
2098 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002099 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002100
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002101 def GetGerritObjForPresubmit(self):
2102 return presubmit_support.GerritAccessor(self._GetGerritHost())
2103
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002104 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002105 """Apply a rough heuristic to give a simple summary of an issue's review
2106 or CQ status, assuming adherence to a common workflow.
2107
2108 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002109 * 'error' - error from review tool (including deleted issues)
2110 * 'unsent' - no reviewers added
2111 * 'waiting' - waiting for review
2112 * 'reply' - waiting for uploader to reply to review
2113 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002114 * 'dry-run' - dry-running in the CQ
2115 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002116 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002117 """
2118 if not self.GetIssue():
2119 return None
2120
2121 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002122 data = self._GetChangeDetail([
2123 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002124 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002125 return 'error'
2126
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002127 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002128 return 'closed'
2129
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002130 cq_label = data['labels'].get('Commit-Queue', {})
2131 max_cq_vote = 0
2132 for vote in cq_label.get('all', []):
2133 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2134 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002135 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002136 if max_cq_vote == 1:
2137 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002138
Aaron Gable9ab38c62017-04-06 14:36:33 -07002139 if data['labels'].get('Code-Review', {}).get('approved'):
2140 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002141
2142 if not data.get('reviewers', {}).get('REVIEWER', []):
2143 return 'unsent'
2144
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002145 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002146 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2147 last_message_author = messages.pop().get('author', {})
2148 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002149 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2150 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002151 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002152 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002153 if last_message_author.get('_account_id') == owner:
2154 # Most recent message was by owner.
2155 return 'waiting'
2156 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002157 # Some reply from non-owner.
2158 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002159
2160 # Somehow there are no messages even though there are reviewers.
2161 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002162
2163 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002164 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002165 patchset = data['revisions'][data['current_revision']]['_number']
2166 self.SetPatchset(patchset)
2167 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002168
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002169 def FetchDescription(self, force=False):
2170 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2171 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002172 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002173 return data['revisions'][current_rev]['commit']['message'].encode(
2174 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175
dsansomee2d6fd92016-09-08 00:10:47 -07002176 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002177 if gerrit_util.HasPendingChangeEdit(
2178 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002179 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002180 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002181 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002182 'unpublished edit. Either publish the edit in the Gerrit web UI '
2183 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002184
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002185 gerrit_util.DeletePendingChangeEdit(
2186 self._GetGerritHost(), self._GerritChangeIdentifier())
2187 gerrit_util.SetCommitMessage(
2188 self._GetGerritHost(), self._GerritChangeIdentifier(),
2189 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002190
Aaron Gable636b13f2017-07-14 10:42:48 -07002191 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002192 gerrit_util.SetReview(
2193 self._GetGerritHost(), self._GerritChangeIdentifier(),
2194 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002195
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002196 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002197 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002198 # CURRENT_REVISION is included to get the latest patchset so that
2199 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002200 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002201 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2202 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002203 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002204 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002205 robot_file_comments = gerrit_util.GetChangeRobotComments(
2206 self._GetGerritHost(), self._GerritChangeIdentifier())
2207
2208 # Add the robot comments onto the list of comments, but only
2209 # keep those that are from the latest pachset.
2210 latest_patch_set = self.GetMostRecentPatchset()
2211 for path, robot_comments in robot_file_comments.iteritems():
2212 line_comments = file_comments.setdefault(path, [])
2213 line_comments.extend(
2214 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002215
2216 # Build dictionary of file comments for easy access and sorting later.
2217 # {author+date: {path: {patchset: {line: url+message}}}}
2218 comments = collections.defaultdict(
2219 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2220 for path, line_comments in file_comments.iteritems():
2221 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002222 tag = comment.get('tag', '')
2223 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002224 continue
2225 key = (comment['author']['email'], comment['updated'])
2226 if comment.get('side', 'REVISION') == 'PARENT':
2227 patchset = 'Base'
2228 else:
2229 patchset = 'PS%d' % comment['patch_set']
2230 line = comment.get('line', 0)
2231 url = ('https://%s/c/%s/%s/%s#%s%s' %
2232 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2233 'b' if comment.get('side') == 'PARENT' else '',
2234 str(line) if line else ''))
2235 comments[key][path][patchset][line] = (url, comment['message'])
2236
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002237 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002238 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002239 summary = self._BuildCommentSummary(msg, comments, readable)
2240 if summary:
2241 summaries.append(summary)
2242 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002243
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002244 @staticmethod
2245 def _BuildCommentSummary(msg, comments, readable):
2246 key = (msg['author']['email'], msg['date'])
2247 # Don't bother showing autogenerated messages that don't have associated
2248 # file or line comments. this will filter out most autogenerated
2249 # messages, but will keep robot comments like those from Tricium.
2250 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2251 if is_autogenerated and not comments.get(key):
2252 return None
2253 message = msg['message']
2254 # Gerrit spits out nanoseconds.
2255 assert len(msg['date'].split('.')[-1]) == 9
2256 date = datetime.datetime.strptime(msg['date'][:-3],
2257 '%Y-%m-%d %H:%M:%S.%f')
2258 if key in comments:
2259 message += '\n'
2260 for path, patchsets in sorted(comments.get(key, {}).items()):
2261 if readable:
2262 message += '\n%s' % path
2263 for patchset, lines in sorted(patchsets.items()):
2264 for line, (url, content) in sorted(lines.items()):
2265 if line:
2266 line_str = 'Line %d' % line
2267 path_str = '%s:%d:' % (path, line)
2268 else:
2269 line_str = 'File comment'
2270 path_str = '%s:0:' % path
2271 if readable:
2272 message += '\n %s, %s: %s' % (patchset, line_str, url)
2273 message += '\n %s\n' % content
2274 else:
2275 message += '\n%s ' % path_str
2276 message += '\n%s\n' % content
2277
2278 return _CommentSummary(
2279 date=date,
2280 message=message,
2281 sender=msg['author']['email'],
2282 autogenerated=is_autogenerated,
2283 # These could be inferred from the text messages and correlated with
2284 # Code-Review label maximum, however this is not reliable.
2285 # Leaving as is until the need arises.
2286 approval=False,
2287 disapproval=False,
2288 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002289
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002291 gerrit_util.AbandonChange(
2292 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002293
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002294 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002295 gerrit_util.SubmitChange(
2296 self._GetGerritHost(), self._GerritChangeIdentifier(),
2297 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002299 def _GetChangeDetail(self, options=None, no_cache=False):
2300 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002301
2302 If fresh data is needed, set no_cache=True which will clear cache and
2303 thus new data will be fetched from Gerrit.
2304 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002306 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002307
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002308 # Optimization to avoid multiple RPCs:
2309 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2310 'CURRENT_COMMIT' not in options):
2311 options.append('CURRENT_COMMIT')
2312
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002313 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002314 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002315 options = [o.upper() for o in options]
2316
2317 # Check in cache first unless no_cache is True.
2318 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002319 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002320 else:
2321 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002322 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002323 # Assumption: data fetched before with extra options is suitable
2324 # for return for a smaller set of options.
2325 # For example, if we cached data for
2326 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2327 # and request is for options=[CURRENT_REVISION],
2328 # THEN we can return prior cached data.
2329 if options_set.issubset(cached_options_set):
2330 return data
2331
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002332 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002333 data = gerrit_util.GetChangeDetail(
2334 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002335 except gerrit_util.GerritError as e:
2336 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002337 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002338 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002339
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002340 self._detail_cache.setdefault(cache_key, []).append(
2341 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002342 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002343
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002344 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002345 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002346 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002347 data = gerrit_util.GetChangeCommit(
2348 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002349 except gerrit_util.GerritError as e:
2350 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002351 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002352 raise
agable32978d92016-11-01 12:55:02 -07002353 return data
2354
Karen Qian40c19422019-03-13 21:28:29 +00002355 def _IsCqConfigured(self):
2356 detail = self._GetChangeDetail(['LABELS'])
2357 if not u'Commit-Queue' in detail.get('labels', {}):
2358 return False
2359 # TODO(crbug/753213): Remove temporary hack
2360 if ('https://chromium.googlesource.com/chromium/src' ==
2361 self._changelist.GetRemoteUrl() and
2362 detail['branch'].startswith('refs/branch-heads/')):
2363 return False
2364 return True
2365
Olivier Robin75ee7252018-04-13 10:02:56 +02002366 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002367 if git_common.is_dirty_git_tree('land'):
2368 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002369
tandriid60367b2016-06-22 05:25:12 -07002370 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002371 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002372 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002373 'which can test and land changes for you. '
2374 'Are you sure you wish to bypass it?\n',
2375 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002376 differs = True
tandriic4344b52016-08-29 06:04:54 -07002377 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002378 # Note: git diff outputs nothing if there is no diff.
2379 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002380 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002381 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002382 if detail['current_revision'] == last_upload:
2383 differs = False
2384 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002385 print('WARNING: Local branch contents differ from latest uploaded '
2386 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002387 if differs:
2388 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002389 confirm_or_exit(
2390 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2391 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002392 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002393 elif not bypass_hooks:
2394 hook_results = self.RunHook(
2395 committing=True,
2396 may_prompt=not force,
2397 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002398 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2399 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002400 if not hook_results.should_continue():
2401 return 1
2402
2403 self.SubmitIssue(wait_for_merge=True)
2404 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002405 links = self._GetChangeCommit().get('web_links', [])
2406 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002407 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002408 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002409 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002410 return 0
2411
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002412 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002413 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002414 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002415 assert not directory
2416 assert parsed_issue_arg.valid
2417
2418 self._changelist.issue = parsed_issue_arg.issue
2419
2420 if parsed_issue_arg.hostname:
2421 self._gerrit_host = parsed_issue_arg.hostname
2422 self._gerrit_server = 'https://%s' % self._gerrit_host
2423
tandriic2405f52016-10-10 08:13:15 -07002424 try:
2425 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002426 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002427 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002428
2429 if not parsed_issue_arg.patchset:
2430 # Use current revision by default.
2431 revision_info = detail['revisions'][detail['current_revision']]
2432 patchset = int(revision_info['_number'])
2433 else:
2434 patchset = parsed_issue_arg.patchset
2435 for revision_info in detail['revisions'].itervalues():
2436 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2437 break
2438 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002439 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002440 (parsed_issue_arg.patchset, self.GetIssue()))
2441
Aaron Gable697a91b2018-01-19 15:20:15 -08002442 remote_url = self._changelist.GetRemoteUrl()
2443 if remote_url.endswith('.git'):
2444 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002445 remote_url = remote_url.rstrip('/')
2446
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002447 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002448 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002449
2450 if remote_url != fetch_info['url']:
2451 DieWithError('Trying to patch a change from %s but this repo appears '
2452 'to be %s.' % (fetch_info['url'], remote_url))
2453
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002454 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002455
Aaron Gable62619a32017-06-16 08:22:09 -07002456 if force:
2457 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2458 print('Checked out commit for change %i patchset %i locally' %
2459 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002460 elif nocommit:
2461 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2462 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002463 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002464 RunGit(['cherry-pick', 'FETCH_HEAD'])
2465 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002466 (parsed_issue_arg.issue, patchset))
2467 print('Note: this created a local commit which does not have '
2468 'the same hash as the one uploaded for review. This will make '
2469 'uploading changes based on top of this branch difficult.\n'
2470 'If you want to do that, use "git cl patch --force" instead.')
2471
Stefan Zagerd08043c2017-10-12 12:07:02 -07002472 if self.GetBranch():
2473 self.SetIssue(parsed_issue_arg.issue)
2474 self.SetPatchset(patchset)
2475 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2476 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2477 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2478 else:
2479 print('WARNING: You are in detached HEAD state.\n'
2480 'The patch has been applied to your checkout, but you will not be '
2481 'able to upload a new patch set to the gerrit issue.\n'
2482 'Try using the \'-b\' option if you would like to work on a '
2483 'branch and/or upload a new patch set.')
2484
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002485 return 0
2486
2487 @staticmethod
2488 def ParseIssueURL(parsed_url):
2489 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2490 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002491 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2492 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002493 # Short urls like https://domain/<issue_number> can be used, but don't allow
2494 # specifying the patchset (you'd 404), but we allow that here.
2495 if parsed_url.path == '/':
2496 part = parsed_url.fragment
2497 else:
2498 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002499 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002500 if match:
2501 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002502 issue=int(match.group(3)),
2503 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002504 hostname=parsed_url.netloc,
2505 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002506 return None
2507
tandrii16e0b4e2016-06-07 10:34:28 -07002508 def _GerritCommitMsgHookCheck(self, offer_removal):
2509 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2510 if not os.path.exists(hook):
2511 return
2512 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2513 # custom developer made one.
2514 data = gclient_utils.FileRead(hook)
2515 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2516 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002517 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002518 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002519 'and may interfere with it in subtle ways.\n'
2520 'We recommend you remove the commit-msg hook.')
2521 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002522 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002523 gclient_utils.rm_file_or_tree(hook)
2524 print('Gerrit commit-msg hook removed.')
2525 else:
2526 print('OK, will keep Gerrit commit-msg hook in place.')
2527
Edward Lemur1b52d872019-05-09 21:12:12 +00002528 def _CleanUpOldTraces(self):
2529 """Keep only the last |MAX_TRACES| traces."""
2530 try:
2531 traces = sorted([
2532 os.path.join(TRACES_DIR, f)
2533 for f in os.listdir(TRACES_DIR)
2534 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2535 and not f.startswith('tmp'))
2536 ])
2537 traces_to_delete = traces[:-MAX_TRACES]
2538 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002539 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002540 except OSError:
2541 print('WARNING: Failed to remove old git traces from\n'
2542 ' %s'
2543 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002544
Edward Lemur5737f022019-05-17 01:24:00 +00002545 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002546 """Zip and write the git push traces stored in traces_dir."""
2547 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002548 traces_zip = trace_name + '-traces'
2549 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002550 # Create a temporary dir to store git config and gitcookies in. It will be
2551 # compressed and stored next to the traces.
2552 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002553 git_info_zip = trace_name + '-git-info'
2554
Edward Lemur5737f022019-05-17 01:24:00 +00002555 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002556 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002557 git_push_metadata['now'] = git_push_metadata['now'].decode(
2558 sys.stdin.encoding)
2559
Edward Lemur1b52d872019-05-09 21:12:12 +00002560 git_push_metadata['trace_name'] = trace_name
2561 gclient_utils.FileWrite(
2562 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2563
2564 # Keep only the first 6 characters of the git hashes on the packet
2565 # trace. This greatly decreases size after compression.
2566 packet_traces = os.path.join(traces_dir, 'trace-packet')
2567 if os.path.isfile(packet_traces):
2568 contents = gclient_utils.FileRead(packet_traces)
2569 gclient_utils.FileWrite(
2570 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2571 shutil.make_archive(traces_zip, 'zip', traces_dir)
2572
2573 # Collect and compress the git config and gitcookies.
2574 git_config = RunGit(['config', '-l'])
2575 gclient_utils.FileWrite(
2576 os.path.join(git_info_dir, 'git-config'),
2577 git_config)
2578
2579 cookie_auth = gerrit_util.Authenticator.get()
2580 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2581 gitcookies_path = cookie_auth.get_gitcookies_path()
2582 if os.path.isfile(gitcookies_path):
2583 gitcookies = gclient_utils.FileRead(gitcookies_path)
2584 gclient_utils.FileWrite(
2585 os.path.join(git_info_dir, 'gitcookies'),
2586 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2587 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2588
Edward Lemur1b52d872019-05-09 21:12:12 +00002589 gclient_utils.rmtree(git_info_dir)
2590
2591 def _RunGitPushWithTraces(
2592 self, change_desc, refspec, refspec_opts, git_push_metadata):
2593 """Run git push and collect the traces resulting from the execution."""
2594 # Create a temporary directory to store traces in. Traces will be compressed
2595 # and stored in a 'traces' dir inside depot_tools.
2596 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002597 trace_name = os.path.join(
2598 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002599
2600 env = os.environ.copy()
2601 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2602 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002603 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002604 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2605 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2606 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2607
2608 try:
2609 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002610 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002611 before_push = time_time()
2612 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002613 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002614 env=env,
2615 print_stdout=True,
2616 # Flush after every line: useful for seeing progress when running as
2617 # recipe.
2618 filter_fn=lambda _: sys.stdout.flush())
2619 except subprocess2.CalledProcessError as e:
2620 push_returncode = e.returncode
2621 DieWithError('Failed to create a change. Please examine output above '
2622 'for the reason of the failure.\n'
2623 'Hint: run command below to diagnose common Git/Gerrit '
2624 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002625 ' git cl creds-check\n'
2626 '\n'
2627 'If git-cl is not working correctly, file a bug under the '
2628 'Infra>SDK component including the files below.\n'
2629 'Review the files before upload, since they might contain '
2630 'sensitive information.\n'
2631 'Set the Restrict-View-Google label so that they are not '
2632 'publicly accessible.\n'
2633 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002634 change_desc)
2635 finally:
2636 execution_time = time_time() - before_push
2637 metrics.collector.add_repeated('sub_commands', {
2638 'command': 'git push',
2639 'execution_time': execution_time,
2640 'exit_code': push_returncode,
2641 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2642 })
2643
Edward Lemur1b52d872019-05-09 21:12:12 +00002644 git_push_metadata['execution_time'] = execution_time
2645 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002646 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002647
Edward Lemur1b52d872019-05-09 21:12:12 +00002648 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002649 gclient_utils.rmtree(traces_dir)
2650
2651 return push_stdout
2652
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002653 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002654 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002655 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002656 # Load default for user, repo, squash=true, in this order.
2657 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002658
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002660 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002661 # This may be None; default fallback value is determined in logic below.
2662 title = options.title
2663
Dominic Battre7d1c4842017-10-27 09:17:28 +02002664 # Extract bug number from branch name.
2665 bug = options.bug
2666 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2667 if not bug and match:
2668 bug = match.group(1)
2669
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002670 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002671 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002672 if self.GetIssue():
2673 # Try to get the message from a previous upload.
2674 message = self.GetDescription()
2675 if not message:
2676 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002677 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002679 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002680 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002681 # When uploading a subsequent patchset, -m|--message is taken
2682 # as the patchset title if --title was not provided.
2683 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002684 else:
2685 default_title = RunGit(
2686 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002687 if options.force:
2688 title = default_title
2689 else:
2690 title = ask_for_data(
2691 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002692 change_id = self._GetChangeDetail()['change_id']
2693 while True:
2694 footer_change_ids = git_footers.get_footer_change_id(message)
2695 if footer_change_ids == [change_id]:
2696 break
2697 if not footer_change_ids:
2698 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002699 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 continue
2701 # There is already a valid footer but with different or several ids.
2702 # Doing this automatically is non-trivial as we don't want to lose
2703 # existing other footers, yet we want to append just 1 desired
2704 # Change-Id. Thus, just create a new footer, but let user verify the
2705 # new description.
2706 message = '%s\n\nChange-Id: %s' % (message, change_id)
2707 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002708 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002710 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002711 'Please, check the proposed correction to the description, '
2712 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2713 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2714 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002715 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002716 if not options.force:
2717 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002718 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002719 message = change_desc.description
2720 if not message:
2721 DieWithError("Description is empty. Aborting...")
2722 # Continue the while loop.
2723 # Sanity check of this code - we should end up with proper message
2724 # footer.
2725 assert [change_id] == git_footers.get_footer_change_id(message)
2726 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002727 else: # if not self.GetIssue()
2728 if options.message:
2729 message = options.message
2730 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002731 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002732 if options.title:
2733 message = options.title + '\n\n' + message
2734 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002735
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002736 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002737 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002738 # On first upload, patchset title is always this string, while
2739 # --title flag gets converted to first line of message.
2740 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 if not change_desc.description:
2742 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002743 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002744 if len(change_ids) > 1:
2745 DieWithError('too many Change-Id footers, at most 1 allowed.')
2746 if not change_ids:
2747 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002748 change_desc.set_description(git_footers.add_footer_change_id(
2749 change_desc.description,
2750 GenerateGerritChangeId(change_desc.description)))
2751 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002752 assert len(change_ids) == 1
2753 change_id = change_ids[0]
2754
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002755 if options.reviewers or options.tbrs or options.add_owners_to:
2756 change_desc.update_reviewers(options.reviewers, options.tbrs,
2757 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002758 if options.preserve_tryjobs:
2759 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002760
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002762 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2763 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002764 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002765 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2766 desc_tempfile.write(change_desc.description)
2767 desc_tempfile.close()
2768 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2769 '-F', desc_tempfile.name]).strip()
2770 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002771 else:
2772 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002773 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002774 if not change_desc.description:
2775 DieWithError("Description is empty. Aborting...")
2776
2777 if not git_footers.get_footer_change_id(change_desc.description):
2778 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002779 change_desc.set_description(
2780 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002781 if options.reviewers or options.tbrs or options.add_owners_to:
2782 change_desc.update_reviewers(options.reviewers, options.tbrs,
2783 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002784 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002785 # For no-squash mode, we assume the remote called "origin" is the one we
2786 # want. It is not worthwhile to support different workflows for
2787 # no-squash mode.
2788 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002789 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2790
2791 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002792 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002793 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2794 ref_to_push)]).splitlines()
2795 if len(commits) > 1:
2796 print('WARNING: This will upload %d commits. Run the following command '
2797 'to see which commits will be uploaded: ' % len(commits))
2798 print('git log %s..%s' % (parent, ref_to_push))
2799 print('You can also use `git squash-branch` to squash these into a '
2800 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002801 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002802
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002803 if options.reviewers or options.tbrs or options.add_owners_to:
2804 change_desc.update_reviewers(options.reviewers, options.tbrs,
2805 options.add_owners_to, change)
2806
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002807 reviewers = sorted(change_desc.get_reviewers())
2808 # Add cc's from the CC_LIST and --cc flag (if any).
2809 if not options.private and not options.no_autocc:
2810 cc = self.GetCCList().split(',')
2811 else:
2812 cc = []
2813 if options.cc:
2814 cc.extend(options.cc)
2815 cc = filter(None, [email.strip() for email in cc])
2816 if change_desc.get_cced():
2817 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002818 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2819 valid_accounts = set(reviewers + cc)
2820 # TODO(crbug/877717): relax this for all hosts.
2821 else:
2822 valid_accounts = gerrit_util.ValidAccounts(
2823 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002824 logging.info('accounts %s are recognized, %s invalid',
2825 sorted(valid_accounts),
2826 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002827
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002828 # Extra options that can be specified at push time. Doc:
2829 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002830 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002831
Aaron Gable844cf292017-06-28 11:32:59 -07002832 # By default, new changes are started in WIP mode, and subsequent patchsets
2833 # don't send email. At any time, passing --send-mail will mark the change
2834 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002835 if options.send_mail:
2836 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002837 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002838 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002839 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002840 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002841 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002842
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002843 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002844 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002845
Aaron Gable9b713dd2016-12-14 16:04:21 -08002846 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002847 # Punctuation and whitespace in |title| must be percent-encoded.
2848 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002849
agablec6787972016-09-09 16:13:34 -07002850 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002851 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002852
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002853 for r in sorted(reviewers):
2854 if r in valid_accounts:
2855 refspec_opts.append('r=%s' % r)
2856 reviewers.remove(r)
2857 else:
2858 # TODO(tandrii): this should probably be a hard failure.
2859 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2860 % r)
2861 for c in sorted(cc):
2862 # refspec option will be rejected if cc doesn't correspond to an
2863 # account, even though REST call to add such arbitrary cc may succeed.
2864 if c in valid_accounts:
2865 refspec_opts.append('cc=%s' % c)
2866 cc.remove(c)
2867
rmistry9eadede2016-09-19 11:22:43 -07002868 if options.topic:
2869 # Documentation on Gerrit topics is here:
2870 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002871 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002872
Edward Lemur687ca902018-12-05 02:30:30 +00002873 if options.enable_auto_submit:
2874 refspec_opts.append('l=Auto-Submit+1')
2875 if options.use_commit_queue:
2876 refspec_opts.append('l=Commit-Queue+2')
2877 elif options.cq_dry_run:
2878 refspec_opts.append('l=Commit-Queue+1')
2879
2880 if change_desc.get_reviewers(tbr_only=True):
2881 score = gerrit_util.GetCodeReviewTbrScore(
2882 self._GetGerritHost(),
2883 self._GetGerritProject())
2884 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002885
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002886 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002887 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002888 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002889 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002890 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2891
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002892 refspec_suffix = ''
2893 if refspec_opts:
2894 refspec_suffix = '%' + ','.join(refspec_opts)
2895 assert ' ' not in refspec_suffix, (
2896 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2897 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2898
Edward Lemur1b52d872019-05-09 21:12:12 +00002899 git_push_metadata = {
2900 'gerrit_host': self._GetGerritHost(),
2901 'title': title or '<untitled>',
2902 'change_id': change_id,
2903 'description': change_desc.description,
2904 }
2905 push_stdout = self._RunGitPushWithTraces(
2906 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907
2908 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002909 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 change_numbers = [m.group(1)
2911 for m in map(regex.match, push_stdout.splitlines())
2912 if m]
2913 if len(change_numbers) != 1:
2914 DieWithError(
2915 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002916 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002917 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002918 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002919
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002920 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002921 # GetIssue() is not set in case of non-squash uploads according to tests.
2922 # TODO(agable): non-squash uploads in git cl should be removed.
2923 gerrit_util.AddReviewers(
2924 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002925 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002926 reviewers, cc,
2927 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002928
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002929 return 0
2930
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002931 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2932 change_desc):
2933 """Computes parent of the generated commit to be uploaded to Gerrit.
2934
2935 Returns revision or a ref name.
2936 """
2937 if custom_cl_base:
2938 # Try to avoid creating additional unintended CLs when uploading, unless
2939 # user wants to take this risk.
2940 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2941 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2942 local_ref_of_target_remote])
2943 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002944 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002945 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2946 'If you proceed with upload, more than 1 CL may be created by '
2947 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2948 'If you are certain that specified base `%s` has already been '
2949 'uploaded to Gerrit as another CL, you may proceed.\n' %
2950 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2951 if not force:
2952 confirm_or_exit(
2953 'Do you take responsibility for cleaning up potential mess '
2954 'resulting from proceeding with upload?',
2955 action='upload')
2956 return custom_cl_base
2957
Aaron Gablef97e33d2017-03-30 15:44:27 -07002958 if remote != '.':
2959 return self.GetCommonAncestorWithUpstream()
2960
2961 # If our upstream branch is local, we base our squashed commit on its
2962 # squashed version.
2963 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2964
Aaron Gablef97e33d2017-03-30 15:44:27 -07002965 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002966 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002967
2968 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002969 # TODO(tandrii): consider checking parent change in Gerrit and using its
2970 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2971 # the tree hash of the parent branch. The upside is less likely bogus
2972 # requests to reupload parent change just because it's uploadhash is
2973 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002974 parent = RunGit(['config',
2975 'branch.%s.gerritsquashhash' % upstream_branch_name],
2976 error_ok=True).strip()
2977 # Verify that the upstream branch has been uploaded too, otherwise
2978 # Gerrit will create additional CLs when uploading.
2979 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2980 RunGitSilent(['rev-parse', parent + ':'])):
2981 DieWithError(
2982 '\nUpload upstream branch %s first.\n'
2983 'It is likely that this branch has been rebased since its last '
2984 'upload, so you just need to upload it again.\n'
2985 '(If you uploaded it with --no-squash, then branch dependencies '
2986 'are not supported, and you should reupload with --squash.)'
2987 % upstream_branch_name,
2988 change_desc)
2989 return parent
2990
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002991 def _AddChangeIdToCommitMessage(self, options, args):
2992 """Re-commits using the current message, assumes the commit hook is in
2993 place.
2994 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002995 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002996 git_command = ['commit', '--amend', '-m', log_desc]
2997 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002998 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002999 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003000 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003001 return new_log_desc
3002 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003003 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003004
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003005 def SetCQState(self, new_state):
3006 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003007 vote_map = {
3008 _CQState.NONE: 0,
3009 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003010 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003011 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003012 labels = {'Commit-Queue': vote_map[new_state]}
3013 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003014 gerrit_util.SetReview(
3015 self._GetGerritHost(), self._GerritChangeIdentifier(),
3016 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003017
tandriie113dfd2016-10-11 10:20:12 -07003018 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003019 try:
3020 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003021 except GerritChangeNotExists:
3022 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003023
3024 if data['status'] in ('ABANDONED', 'MERGED'):
3025 return 'CL %s is closed' % self.GetIssue()
3026
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003027 def GetTryJobProperties(self, patchset=None):
3028 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003029 data = self._GetChangeDetail(['ALL_REVISIONS'])
3030 patchset = int(patchset or self.GetPatchset())
3031 assert patchset
3032 revision_data = None # Pylint wants it to be defined.
3033 for revision_data in data['revisions'].itervalues():
3034 if int(revision_data['_number']) == patchset:
3035 break
3036 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003037 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003038 (patchset, self.GetIssue()))
3039 return {
3040 'patch_issue': self.GetIssue(),
3041 'patch_set': patchset or self.GetPatchset(),
3042 'patch_project': data['project'],
3043 'patch_storage': 'gerrit',
3044 'patch_ref': revision_data['fetch']['http']['ref'],
3045 'patch_repository_url': revision_data['fetch']['http']['url'],
3046 'patch_gerrit_url': self.GetCodereviewServer(),
3047 }
tandriie113dfd2016-10-11 10:20:12 -07003048
tandriide281ae2016-10-12 06:02:30 -07003049 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003050 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003051
Edward Lemur707d70b2018-02-07 00:50:14 +01003052 def GetReviewers(self):
3053 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003054 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003055
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003056
3057_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003058 'gerrit': _GerritChangelistImpl,
3059}
3060
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003061
iannuccie53c9352016-08-17 14:40:40 -07003062def _add_codereview_issue_select_options(parser, extra=""):
3063 _add_codereview_select_options(parser)
3064
3065 text = ('Operate on this issue number instead of the current branch\'s '
3066 'implicit issue.')
3067 if extra:
3068 text += ' '+extra
3069 parser.add_option('-i', '--issue', type=int, help=text)
3070
3071
3072def _process_codereview_issue_select_options(parser, options):
3073 _process_codereview_select_options(parser, options)
3074 if options.issue is not None and not options.forced_codereview:
3075 parser.error('--issue must be specified with either --rietveld or --gerrit')
3076
3077
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003078def _add_codereview_select_options(parser):
3079 """Appends --gerrit and --rietveld options to force specific codereview."""
3080 parser.codereview_group = optparse.OptionGroup(
3081 parser, 'EXPERIMENTAL! Codereview override options')
3082 parser.add_option_group(parser.codereview_group)
3083 parser.codereview_group.add_option(
3084 '--gerrit', action='store_true',
3085 help='Force the use of Gerrit for codereview')
3086 parser.codereview_group.add_option(
3087 '--rietveld', action='store_true',
3088 help='Force the use of Rietveld for codereview')
3089
3090
3091def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003092 if options.rietveld:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003093 parser.error('--rietveld is no longer supported.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003094 options.forced_codereview = None
3095 if options.gerrit:
3096 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003097
3098
tandriif9aefb72016-07-01 09:06:51 -07003099def _get_bug_line_values(default_project, bugs):
3100 """Given default_project and comma separated list of bugs, yields bug line
3101 values.
3102
3103 Each bug can be either:
3104 * a number, which is combined with default_project
3105 * string, which is left as is.
3106
3107 This function may produce more than one line, because bugdroid expects one
3108 project per line.
3109
3110 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3111 ['v8:123', 'chromium:789']
3112 """
3113 default_bugs = []
3114 others = []
3115 for bug in bugs.split(','):
3116 bug = bug.strip()
3117 if bug:
3118 try:
3119 default_bugs.append(int(bug))
3120 except ValueError:
3121 others.append(bug)
3122
3123 if default_bugs:
3124 default_bugs = ','.join(map(str, default_bugs))
3125 if default_project:
3126 yield '%s:%s' % (default_project, default_bugs)
3127 else:
3128 yield default_bugs
3129 for other in sorted(others):
3130 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3131 yield other
3132
3133
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003134class ChangeDescription(object):
3135 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003136 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003137 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003138 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003139 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003140 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3141 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3142 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3143 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003144
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003145 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003146 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147
agable@chromium.org42c20792013-09-12 17:34:49 +00003148 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003149 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003150 return '\n'.join(self._description_lines)
3151
3152 def set_description(self, desc):
3153 if isinstance(desc, basestring):
3154 lines = desc.splitlines()
3155 else:
3156 lines = [line.rstrip() for line in desc]
3157 while lines and not lines[0]:
3158 lines.pop(0)
3159 while lines and not lines[-1]:
3160 lines.pop(-1)
3161 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003162
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003163 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3164 """Rewrites the R=/TBR= line(s) as a single line each.
3165
3166 Args:
3167 reviewers (list(str)) - list of additional emails to use for reviewers.
3168 tbrs (list(str)) - list of additional emails to use for TBRs.
3169 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3170 the change that are missing OWNER coverage. If this is not None, you
3171 must also pass a value for `change`.
3172 change (Change) - The Change that should be used for OWNERS lookups.
3173 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003174 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003175 assert isinstance(tbrs, list), tbrs
3176
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003177 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003178 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003179
3180 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003181 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003182
3183 reviewers = set(reviewers)
3184 tbrs = set(tbrs)
3185 LOOKUP = {
3186 'TBR': tbrs,
3187 'R': reviewers,
3188 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003189
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003190 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003191 regexp = re.compile(self.R_LINE)
3192 matches = [regexp.match(line) for line in self._description_lines]
3193 new_desc = [l for i, l in enumerate(self._description_lines)
3194 if not matches[i]]
3195 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003196
agable@chromium.org42c20792013-09-12 17:34:49 +00003197 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003198
3199 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003200 for match in matches:
3201 if not match:
3202 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003203 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3204
3205 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003206 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003207 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003208 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003209 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003210 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003211 LOOKUP[add_owners_to].update(
3212 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003213
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003214 # If any folks ended up in both groups, remove them from tbrs.
3215 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003216
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003217 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3218 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003219
3220 # Put the new lines in the description where the old first R= line was.
3221 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3222 if 0 <= line_loc < len(self._description_lines):
3223 if new_tbr_line:
3224 self._description_lines.insert(line_loc, new_tbr_line)
3225 if new_r_line:
3226 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003227 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003228 if new_r_line:
3229 self.append_footer(new_r_line)
3230 if new_tbr_line:
3231 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003232
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003233 def set_preserve_tryjobs(self):
3234 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3235 footers = git_footers.parse_footers(self.description)
3236 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3237 if v.lower() == 'true':
3238 return
3239 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3240
Aaron Gable3a16ed12017-03-23 10:51:55 -07003241 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003242 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003243 self.set_description([
3244 '# Enter a description of the change.',
3245 '# This will be displayed on the codereview site.',
3246 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003247 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003248 '--------------------',
3249 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003250
agable@chromium.org42c20792013-09-12 17:34:49 +00003251 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003252 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003253 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003254 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003255 if git_footer:
3256 self.append_footer('Bug: %s' % ', '.join(values))
3257 else:
3258 for value in values:
3259 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003260
agable@chromium.org42c20792013-09-12 17:34:49 +00003261 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003262 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003263 if not content:
3264 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003265 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003266
Bruce Dawson2377b012018-01-11 16:46:49 -08003267 # Strip off comments and default inserted "Bug:" line.
3268 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003269 (line.startswith('#') or
3270 line.rstrip() == "Bug:" or
3271 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003272 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003273 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003274 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003275
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003276 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003277 """Adds a footer line to the description.
3278
3279 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3280 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3281 that Gerrit footers are always at the end.
3282 """
3283 parsed_footer_line = git_footers.parse_footer(line)
3284 if parsed_footer_line:
3285 # Line is a gerrit footer in the form: Footer-Key: any value.
3286 # Thus, must be appended observing Gerrit footer rules.
3287 self.set_description(
3288 git_footers.add_footer(self.description,
3289 key=parsed_footer_line[0],
3290 value=parsed_footer_line[1]))
3291 return
3292
3293 if not self._description_lines:
3294 self._description_lines.append(line)
3295 return
3296
3297 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3298 if gerrit_footers:
3299 # git_footers.split_footers ensures that there is an empty line before
3300 # actual (gerrit) footers, if any. We have to keep it that way.
3301 assert top_lines and top_lines[-1] == ''
3302 top_lines, separator = top_lines[:-1], top_lines[-1:]
3303 else:
3304 separator = [] # No need for separator if there are no gerrit_footers.
3305
3306 prev_line = top_lines[-1] if top_lines else ''
3307 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3308 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3309 top_lines.append('')
3310 top_lines.append(line)
3311 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003312
tandrii99a72f22016-08-17 14:33:24 -07003313 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003314 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003315 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003316 reviewers = [match.group(2).strip()
3317 for match in matches
3318 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003319 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003320
bradnelsond975b302016-10-23 12:20:23 -07003321 def get_cced(self):
3322 """Retrieves the list of reviewers."""
3323 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3324 cced = [match.group(2).strip() for match in matches if match]
3325 return cleanup_list(cced)
3326
Nodir Turakulov23b82142017-11-16 11:04:25 -08003327 def get_hash_tags(self):
3328 """Extracts and sanitizes a list of Gerrit hashtags."""
3329 subject = (self._description_lines or ('',))[0]
3330 subject = re.sub(
3331 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3332
3333 tags = []
3334 start = 0
3335 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3336 while True:
3337 m = bracket_exp.match(subject, start)
3338 if not m:
3339 break
3340 tags.append(self.sanitize_hash_tag(m.group(1)))
3341 start = m.end()
3342
3343 if not tags:
3344 # Try "Tag: " prefix.
3345 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3346 if m:
3347 tags.append(self.sanitize_hash_tag(m.group(1)))
3348 return tags
3349
3350 @classmethod
3351 def sanitize_hash_tag(cls, tag):
3352 """Returns a sanitized Gerrit hash tag.
3353
3354 A sanitized hashtag can be used as a git push refspec parameter value.
3355 """
3356 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3357
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003358 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3359 """Updates this commit description given the parent.
3360
3361 This is essentially what Gnumbd used to do.
3362 Consult https://goo.gl/WMmpDe for more details.
3363 """
3364 assert parent_msg # No, orphan branch creation isn't supported.
3365 assert parent_hash
3366 assert dest_ref
3367 parent_footer_map = git_footers.parse_footers(parent_msg)
3368 # This will also happily parse svn-position, which GnumbD is no longer
3369 # supporting. While we'd generate correct footers, the verifier plugin
3370 # installed in Gerrit will block such commit (ie git push below will fail).
3371 parent_position = git_footers.get_position(parent_footer_map)
3372
3373 # Cherry-picks may have last line obscuring their prior footers,
3374 # from git_footers perspective. This is also what Gnumbd did.
3375 cp_line = None
3376 if (self._description_lines and
3377 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3378 cp_line = self._description_lines.pop()
3379
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003380 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003381
3382 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3383 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003384 for i, line in enumerate(footer_lines):
3385 k, v = git_footers.parse_footer(line) or (None, None)
3386 if k and k.startswith('Cr-'):
3387 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003388
3389 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003390 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003391 if parent_position[0] == dest_ref:
3392 # Same branch as parent.
3393 number = int(parent_position[1]) + 1
3394 else:
3395 number = 1 # New branch, and extra lineage.
3396 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3397 int(parent_position[1])))
3398
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003399 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3400 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003401
3402 self._description_lines = top_lines
3403 if cp_line:
3404 self._description_lines.append(cp_line)
3405 if self._description_lines[-1] != '':
3406 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003407 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003408
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003409
Aaron Gablea1bab272017-04-11 16:38:18 -07003410def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003411 """Retrieves the reviewers that approved a CL from the issue properties with
3412 messages.
3413
3414 Note that the list may contain reviewers that are not committer, thus are not
3415 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003416
3417 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003418 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003419 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003420 return sorted(
3421 set(
3422 message['sender']
3423 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003424 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003425 )
3426 )
3427
3428
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003429def FindCodereviewSettingsFile(filename='codereview.settings'):
3430 """Finds the given file starting in the cwd and going up.
3431
3432 Only looks up to the top of the repository unless an
3433 'inherit-review-settings-ok' file exists in the root of the repository.
3434 """
3435 inherit_ok_file = 'inherit-review-settings-ok'
3436 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003437 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003438 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3439 root = '/'
3440 while True:
3441 if filename in os.listdir(cwd):
3442 if os.path.isfile(os.path.join(cwd, filename)):
3443 return open(os.path.join(cwd, filename))
3444 if cwd == root:
3445 break
3446 cwd = os.path.dirname(cwd)
3447
3448
3449def LoadCodereviewSettingsFromFile(fileobj):
3450 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003451 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003453 def SetProperty(name, setting, unset_error_ok=False):
3454 fullname = 'rietveld.' + name
3455 if setting in keyvals:
3456 RunGit(['config', fullname, keyvals[setting]])
3457 else:
3458 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3459
tandrii48df5812016-10-17 03:55:37 -07003460 if not keyvals.get('GERRIT_HOST', False):
3461 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462 # Only server setting is required. Other settings can be absent.
3463 # In that case, we ignore errors raised during option deletion attempt.
3464 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3465 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3466 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003467 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003468 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3469 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003470 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3471 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003472
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003473 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003474 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003475
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003476 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003477 RunGit(['config', 'gerrit.squash-uploads',
3478 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003479
tandrii@chromium.org28253532016-04-14 13:46:56 +00003480 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003481 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003482 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3483
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003484 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003485 # should be of the form
3486 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3487 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003488 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3489 keyvals['ORIGIN_URL_CONFIG']])
3490
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003491
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003492def urlretrieve(source, destination):
3493 """urllib is broken for SSL connections via a proxy therefore we
3494 can't use urllib.urlretrieve()."""
3495 with open(destination, 'w') as f:
3496 f.write(urllib2.urlopen(source).read())
3497
3498
ukai@chromium.org712d6102013-11-27 00:52:58 +00003499def hasSheBang(fname):
3500 """Checks fname is a #! script."""
3501 with open(fname) as f:
3502 return f.read(2).startswith('#!')
3503
3504
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003505# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3506def DownloadHooks(*args, **kwargs):
3507 pass
3508
3509
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003510def DownloadGerritHook(force):
3511 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003512
3513 Args:
3514 force: True to update hooks. False to install hooks if not present.
3515 """
3516 if not settings.GetIsGerrit():
3517 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003518 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003519 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3520 if not os.access(dst, os.X_OK):
3521 if os.path.exists(dst):
3522 if not force:
3523 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003524 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003525 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003526 if not hasSheBang(dst):
3527 DieWithError('Not a script: %s\n'
3528 'You need to download from\n%s\n'
3529 'into .git/hooks/commit-msg and '
3530 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003531 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3532 except Exception:
3533 if os.path.exists(dst):
3534 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003535 DieWithError('\nFailed to download hooks.\n'
3536 'You need to download from\n%s\n'
3537 'into .git/hooks/commit-msg and '
3538 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003539
3540
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003541class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003542 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003543
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003544 _GOOGLESOURCE = 'googlesource.com'
3545
3546 def __init__(self):
3547 # Cached list of [host, identity, source], where source is either
3548 # .gitcookies or .netrc.
3549 self._all_hosts = None
3550
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003551 def ensure_configured_gitcookies(self):
3552 """Runs checks and suggests fixes to make git use .gitcookies from default
3553 path."""
3554 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3555 configured_path = RunGitSilent(
3556 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003557 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003558 if configured_path:
3559 self._ensure_default_gitcookies_path(configured_path, default)
3560 else:
3561 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003562
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003563 @staticmethod
3564 def _ensure_default_gitcookies_path(configured_path, default_path):
3565 assert configured_path
3566 if configured_path == default_path:
3567 print('git is already configured to use your .gitcookies from %s' %
3568 configured_path)
3569 return
3570
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003571 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003572 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3573 (configured_path, default_path))
3574
3575 if not os.path.exists(configured_path):
3576 print('However, your configured .gitcookies file is missing.')
3577 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3578 action='reconfigure')
3579 RunGit(['config', '--global', 'http.cookiefile', default_path])
3580 return
3581
3582 if os.path.exists(default_path):
3583 print('WARNING: default .gitcookies file already exists %s' %
3584 default_path)
3585 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3586 default_path)
3587
3588 confirm_or_exit('Move existing .gitcookies to default location?',
3589 action='move')
3590 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003591 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003592 print('Moved and reconfigured git to use .gitcookies from %s' %
3593 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003594
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003595 @staticmethod
3596 def _configure_gitcookies_path(default_path):
3597 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3598 if os.path.exists(netrc_path):
3599 print('You seem to be using outdated .netrc for git credentials: %s' %
3600 netrc_path)
3601 print('This tool will guide you through setting up recommended '
3602 '.gitcookies store for git credentials.\n'
3603 '\n'
3604 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3605 ' git config --global --unset http.cookiefile\n'
3606 ' mv %s %s.backup\n\n' % (default_path, default_path))
3607 confirm_or_exit(action='setup .gitcookies')
3608 RunGit(['config', '--global', 'http.cookiefile', default_path])
3609 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003610
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003611 def get_hosts_with_creds(self, include_netrc=False):
3612 if self._all_hosts is None:
3613 a = gerrit_util.CookiesAuthenticator()
3614 self._all_hosts = [
3615 (h, u, s)
3616 for h, u, s in itertools.chain(
3617 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3618 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3619 )
3620 if h.endswith(self._GOOGLESOURCE)
3621 ]
3622
3623 if include_netrc:
3624 return self._all_hosts
3625 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3626
3627 def print_current_creds(self, include_netrc=False):
3628 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3629 if not hosts:
3630 print('No Git/Gerrit credentials found')
3631 return
3632 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3633 header = [('Host', 'User', 'Which file'),
3634 ['=' * l for l in lengths]]
3635 for row in (header + hosts):
3636 print('\t'.join((('%%+%ds' % l) % s)
3637 for l, s in zip(lengths, row)))
3638
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003639 @staticmethod
3640 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003641 """Parses identity "git-<username>.domain" into <username> and domain."""
3642 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003643 # distinguishable from sub-domains. But we do know typical domains:
3644 if identity.endswith('.chromium.org'):
3645 domain = 'chromium.org'
3646 username = identity[:-len('.chromium.org')]
3647 else:
3648 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003649 if username.startswith('git-'):
3650 username = username[len('git-'):]
3651 return username, domain
3652
3653 def _get_usernames_of_domain(self, domain):
3654 """Returns list of usernames referenced by .gitcookies in a given domain."""
3655 identities_by_domain = {}
3656 for _, identity, _ in self.get_hosts_with_creds():
3657 username, domain = self._parse_identity(identity)
3658 identities_by_domain.setdefault(domain, []).append(username)
3659 return identities_by_domain.get(domain)
3660
3661 def _canonical_git_googlesource_host(self, host):
3662 """Normalizes Gerrit hosts (with '-review') to Git host."""
3663 assert host.endswith(self._GOOGLESOURCE)
3664 # Prefix doesn't include '.' at the end.
3665 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3666 if prefix.endswith('-review'):
3667 prefix = prefix[:-len('-review')]
3668 return prefix + '.' + self._GOOGLESOURCE
3669
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003670 def _canonical_gerrit_googlesource_host(self, host):
3671 git_host = self._canonical_git_googlesource_host(host)
3672 prefix = git_host.split('.', 1)[0]
3673 return prefix + '-review.' + self._GOOGLESOURCE
3674
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003675 def _get_counterpart_host(self, host):
3676 assert host.endswith(self._GOOGLESOURCE)
3677 git = self._canonical_git_googlesource_host(host)
3678 gerrit = self._canonical_gerrit_googlesource_host(git)
3679 return git if gerrit == host else gerrit
3680
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003681 def has_generic_host(self):
3682 """Returns whether generic .googlesource.com has been configured.
3683
3684 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3685 """
3686 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3687 if host == '.' + self._GOOGLESOURCE:
3688 return True
3689 return False
3690
3691 def _get_git_gerrit_identity_pairs(self):
3692 """Returns map from canonic host to pair of identities (Git, Gerrit).
3693
3694 One of identities might be None, meaning not configured.
3695 """
3696 host_to_identity_pairs = {}
3697 for host, identity, _ in self.get_hosts_with_creds():
3698 canonical = self._canonical_git_googlesource_host(host)
3699 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3700 idx = 0 if canonical == host else 1
3701 pair[idx] = identity
3702 return host_to_identity_pairs
3703
3704 def get_partially_configured_hosts(self):
3705 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003706 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3707 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3708 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003709
3710 def get_conflicting_hosts(self):
3711 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003712 host
3713 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003714 if None not in (i1, i2) and i1 != i2)
3715
3716 def get_duplicated_hosts(self):
3717 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3718 return set(host for host, count in counters.iteritems() if count > 1)
3719
3720 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3721 'chromium.googlesource.com': 'chromium.org',
3722 'chrome-internal.googlesource.com': 'google.com',
3723 }
3724
3725 def get_hosts_with_wrong_identities(self):
3726 """Finds hosts which **likely** reference wrong identities.
3727
3728 Note: skips hosts which have conflicting identities for Git and Gerrit.
3729 """
3730 hosts = set()
3731 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3732 pair = self._get_git_gerrit_identity_pairs().get(host)
3733 if pair and pair[0] == pair[1]:
3734 _, domain = self._parse_identity(pair[0])
3735 if domain != expected:
3736 hosts.add(host)
3737 return hosts
3738
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003739 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003740 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003741 hosts = sorted(hosts)
3742 assert hosts
3743 if extra_column_func is None:
3744 extras = [''] * len(hosts)
3745 else:
3746 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003747 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3748 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003749 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003750 lines.append(tmpl % he)
3751 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003752
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003753 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003754 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003755 yield ('.googlesource.com wildcard record detected',
3756 ['Chrome Infrastructure team recommends to list full host names '
3757 'explicitly.'],
3758 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003759
3760 dups = self.get_duplicated_hosts()
3761 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003762 yield ('The following hosts were defined twice',
3763 self._format_hosts(dups),
3764 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003765
3766 partial = self.get_partially_configured_hosts()
3767 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003768 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3769 'These hosts are missing',
3770 self._format_hosts(partial, lambda host: 'but %s defined' %
3771 self._get_counterpart_host(host)),
3772 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003773
3774 conflicting = self.get_conflicting_hosts()
3775 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003776 yield ('The following Git hosts have differing credentials from their '
3777 'Gerrit counterparts',
3778 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3779 tuple(self._get_git_gerrit_identity_pairs()[host])),
3780 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003781
3782 wrong = self.get_hosts_with_wrong_identities()
3783 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003784 yield ('These hosts likely use wrong identity',
3785 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3786 (self._get_git_gerrit_identity_pairs()[host][0],
3787 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3788 wrong)
3789
3790 def find_and_report_problems(self):
3791 """Returns True if there was at least one problem, else False."""
3792 found = False
3793 bad_hosts = set()
3794 for title, sublines, hosts in self._find_problems():
3795 if not found:
3796 found = True
3797 print('\n\n.gitcookies problem report:\n')
3798 bad_hosts.update(hosts or [])
3799 print(' %s%s' % (title , (':' if sublines else '')))
3800 if sublines:
3801 print()
3802 print(' %s' % '\n '.join(sublines))
3803 print()
3804
3805 if bad_hosts:
3806 assert found
3807 print(' You can manually remove corresponding lines in your %s file and '
3808 'visit the following URLs with correct account to generate '
3809 'correct credential lines:\n' %
3810 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3811 print(' %s' % '\n '.join(sorted(set(
3812 gerrit_util.CookiesAuthenticator().get_new_password_url(
3813 self._canonical_git_googlesource_host(host))
3814 for host in bad_hosts
3815 ))))
3816 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003817
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003818
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003819@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003820def CMDcreds_check(parser, args):
3821 """Checks credentials and suggests changes."""
3822 _, _ = parser.parse_args(args)
3823
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003824 # Code below checks .gitcookies. Abort if using something else.
3825 authn = gerrit_util.Authenticator.get()
3826 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3827 if isinstance(authn, gerrit_util.GceAuthenticator):
3828 DieWithError(
3829 'This command is not designed for GCE, are you on a bot?\n'
3830 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3831 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003832 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003833 'This command is not designed for bot environment. It checks '
3834 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003835
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003836 checker = _GitCookiesChecker()
3837 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003838
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003839 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003840 checker.print_current_creds(include_netrc=True)
3841
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003842 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003843 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003844 return 0
3845 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003846
3847
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003848@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003849def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003850 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003851 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3852 branch = ShortBranchName(branchref)
3853 _, args = parser.parse_args(args)
3854 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003855 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003856 return RunGit(['config', 'branch.%s.base-url' % branch],
3857 error_ok=False).strip()
3858 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003859 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003860 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3861 error_ok=False).strip()
3862
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003863def color_for_status(status):
3864 """Maps a Changelist status to color, for CMDstatus and other tools."""
3865 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003866 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003867 'waiting': Fore.BLUE,
3868 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003869 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003870 'lgtm': Fore.GREEN,
3871 'commit': Fore.MAGENTA,
3872 'closed': Fore.CYAN,
3873 'error': Fore.WHITE,
3874 }.get(status, Fore.WHITE)
3875
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003876
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003877def get_cl_statuses(changes, fine_grained, max_processes=None):
3878 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003879
3880 If fine_grained is true, this will fetch CL statuses from the server.
3881 Otherwise, simply indicate if there's a matching url for the given branches.
3882
3883 If max_processes is specified, it is used as the maximum number of processes
3884 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3885 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003886
3887 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003888 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003889 if not changes:
3890 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003891
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003892 if not fine_grained:
3893 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003894 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003895 for cl in changes:
3896 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003897 return
3898
3899 # First, sort out authentication issues.
3900 logging.debug('ensuring credentials exist')
3901 for cl in changes:
3902 cl.EnsureAuthenticated(force=False, refresh=True)
3903
3904 def fetch(cl):
3905 try:
3906 return (cl, cl.GetStatus())
3907 except:
3908 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003909 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003910 raise
3911
3912 threads_count = len(changes)
3913 if max_processes:
3914 threads_count = max(1, min(threads_count, max_processes))
3915 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3916
3917 pool = ThreadPool(threads_count)
3918 fetched_cls = set()
3919 try:
3920 it = pool.imap_unordered(fetch, changes).__iter__()
3921 while True:
3922 try:
3923 cl, status = it.next(timeout=5)
3924 except multiprocessing.TimeoutError:
3925 break
3926 fetched_cls.add(cl)
3927 yield cl, status
3928 finally:
3929 pool.close()
3930
3931 # Add any branches that failed to fetch.
3932 for cl in set(changes) - fetched_cls:
3933 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003934
rmistry@google.com2dd99862015-06-22 12:22:18 +00003935
3936def upload_branch_deps(cl, args):
3937 """Uploads CLs of local branches that are dependents of the current branch.
3938
3939 If the local branch dependency tree looks like:
3940 test1 -> test2.1 -> test3.1
3941 -> test3.2
3942 -> test2.2 -> test3.3
3943
3944 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3945 run on the dependent branches in this order:
3946 test2.1, test3.1, test3.2, test2.2, test3.3
3947
3948 Note: This function does not rebase your local dependent branches. Use it when
3949 you make a change to the parent branch that will not conflict with its
3950 dependent branches, and you would like their dependencies updated in
3951 Rietveld.
3952 """
3953 if git_common.is_dirty_git_tree('upload-branch-deps'):
3954 return 1
3955
3956 root_branch = cl.GetBranch()
3957 if root_branch is None:
3958 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3959 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003960 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003961 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3962 'patchset dependencies without an uploaded CL.')
3963
3964 branches = RunGit(['for-each-ref',
3965 '--format=%(refname:short) %(upstream:short)',
3966 'refs/heads'])
3967 if not branches:
3968 print('No local branches found.')
3969 return 0
3970
3971 # Create a dictionary of all local branches to the branches that are dependent
3972 # on it.
3973 tracked_to_dependents = collections.defaultdict(list)
3974 for b in branches.splitlines():
3975 tokens = b.split()
3976 if len(tokens) == 2:
3977 branch_name, tracked = tokens
3978 tracked_to_dependents[tracked].append(branch_name)
3979
vapiera7fbd5a2016-06-16 09:17:49 -07003980 print()
3981 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003982 dependents = []
3983 def traverse_dependents_preorder(branch, padding=''):
3984 dependents_to_process = tracked_to_dependents.get(branch, [])
3985 padding += ' '
3986 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003987 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003988 dependents.append(dependent)
3989 traverse_dependents_preorder(dependent, padding)
3990 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003991 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003992
3993 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003994 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003995 return 0
3996
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003997 confirm_or_exit('This command will checkout all dependent branches and run '
3998 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003999
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000 # Record all dependents that failed to upload.
4001 failures = {}
4002 # Go through all dependents, checkout the branch and upload.
4003 try:
4004 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004005 print()
4006 print('--------------------------------------')
4007 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004008 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004009 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004010 try:
4011 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004012 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004013 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004014 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004015 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004016 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004017 finally:
4018 # Swap back to the original root branch.
4019 RunGit(['checkout', '-q', root_branch])
4020
vapiera7fbd5a2016-06-16 09:17:49 -07004021 print()
4022 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004023 for dependent_branch in dependents:
4024 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004025 print(' %s : %s' % (dependent_branch, upload_status))
4026 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004027
4028 return 0
4029
4030
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004031@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004032def CMDarchive(parser, args):
4033 """Archives and deletes branches associated with closed changelists."""
4034 parser.add_option(
4035 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004036 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004037 parser.add_option(
4038 '-f', '--force', action='store_true',
4039 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004040 parser.add_option(
4041 '-d', '--dry-run', action='store_true',
4042 help='Skip the branch tagging and removal steps.')
4043 parser.add_option(
4044 '-t', '--notags', action='store_true',
4045 help='Do not tag archived branches. '
4046 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004047
4048 auth.add_auth_options(parser)
4049 options, args = parser.parse_args(args)
4050 if args:
4051 parser.error('Unsupported args: %s' % ' '.join(args))
4052 auth_config = auth.extract_auth_config_from_options(options)
4053
4054 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4055 if not branches:
4056 return 0
4057
vapiera7fbd5a2016-06-16 09:17:49 -07004058 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004059 changes = [Changelist(branchref=b, auth_config=auth_config)
4060 for b in branches.splitlines()]
4061 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4062 statuses = get_cl_statuses(changes,
4063 fine_grained=True,
4064 max_processes=options.maxjobs)
4065 proposal = [(cl.GetBranch(),
4066 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4067 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004068 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004069 proposal.sort()
4070
4071 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004072 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004073 return 0
4074
4075 current_branch = GetCurrentBranch()
4076
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004078 if options.notags:
4079 for next_item in proposal:
4080 print(' ' + next_item[0])
4081 else:
4082 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4083 for next_item in proposal:
4084 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004085
kmarshall9249e012016-08-23 12:02:16 -07004086 # Quit now on precondition failure or if instructed by the user, either
4087 # via an interactive prompt or by command line flags.
4088 if options.dry_run:
4089 print('\nNo changes were made (dry run).\n')
4090 return 0
4091 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004092 print('You are currently on a branch \'%s\' which is associated with a '
4093 'closed codereview issue, so archive cannot proceed. Please '
4094 'checkout another branch and run this command again.' %
4095 current_branch)
4096 return 1
kmarshall9249e012016-08-23 12:02:16 -07004097 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004098 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4099 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004100 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004101 return 1
4102
4103 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004104 if not options.notags:
4105 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004106 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004107
vapiera7fbd5a2016-06-16 09:17:49 -07004108 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004109
4110 return 0
4111
4112
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004113@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004115 """Show status of changelists.
4116
4117 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004118 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004119 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004120 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004121 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004122 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004123 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004124 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004125
4126 Also see 'git cl comments'.
4127 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004128 parser.add_option(
4129 '--no-branch-color',
4130 action='store_true',
4131 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004132 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004133 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004134 parser.add_option('-f', '--fast', action='store_true',
4135 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004136 parser.add_option(
4137 '-j', '--maxjobs', action='store', type=int,
4138 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004139
4140 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004141 _add_codereview_issue_select_options(
4142 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004143 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004144 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004145 if args:
4146 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004147 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004148
iannuccie53c9352016-08-17 14:40:40 -07004149 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004150 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07004151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004152 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004153 cl = Changelist(auth_config=auth_config, issue=options.issue,
4154 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004155 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004157 elif options.field == 'id':
4158 issueid = cl.GetIssue()
4159 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004161 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004162 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004163 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004164 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004165 elif options.field == 'status':
4166 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004167 elif options.field == 'url':
4168 url = cl.GetIssueURL()
4169 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004171 return 0
4172
4173 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4174 if not branches:
4175 print('No local branch found.')
4176 return 0
4177
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004178 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004179 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004180 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004181 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004182 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004183 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004184 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004185
Daniel McArdlea23bf592019-02-12 00:25:12 +00004186 current_branch = GetCurrentBranch()
4187
4188 def FormatBranchName(branch, colorize=False):
4189 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4190 an asterisk when it is the current branch."""
4191
4192 asterisk = ""
4193 color = Fore.RESET
4194 if branch == current_branch:
4195 asterisk = "* "
4196 color = Fore.GREEN
4197 branch_name = ShortBranchName(branch)
4198
4199 if colorize:
4200 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004201 return asterisk + branch_name
4202
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004203 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004204
4205 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004206 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4207 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004208 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004209 c, status = output.next()
4210 branch_statuses[c.GetBranch()] = status
4211 status = branch_statuses.pop(branch)
4212 url = cl.GetIssueURL()
4213 if url and (not status or status == 'error'):
4214 # The issue probably doesn't exist anymore.
4215 url += ' (broken)'
4216
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004217 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004218 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004219 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004220 color = ''
4221 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004222 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004223
Alan Cuttera3be9a52019-03-04 18:50:33 +00004224 branch_display = FormatBranchName(branch)
4225 padding = ' ' * (alignment - len(branch_display))
4226 if not options.no_branch_color:
4227 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004228
Alan Cuttera3be9a52019-03-04 18:50:33 +00004229 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4230 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004231
vapiera7fbd5a2016-06-16 09:17:49 -07004232 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004233 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004234 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004235 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004236 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004237 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004238 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004239 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004240 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004241 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004242 print('Issue description:')
4243 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004244 return 0
4245
4246
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004247def colorize_CMDstatus_doc():
4248 """To be called once in main() to add colors to git cl status help."""
4249 colors = [i for i in dir(Fore) if i[0].isupper()]
4250
4251 def colorize_line(line):
4252 for color in colors:
4253 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004254 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004255 indent = len(line) - len(line.lstrip(' ')) + 1
4256 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4257 return line
4258
4259 lines = CMDstatus.__doc__.splitlines()
4260 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4261
4262
phajdan.jre328cf92016-08-22 04:12:17 -07004263def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004264 if path == '-':
4265 json.dump(contents, sys.stdout)
4266 else:
4267 with open(path, 'w') as f:
4268 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004269
4270
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004271@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004272@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004273def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004274 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275
4276 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004277 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004278 parser.add_option('-r', '--reverse', action='store_true',
4279 help='Lookup the branch(es) for the specified issues. If '
4280 'no issues are specified, all branches with mapped '
4281 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004282 parser.add_option('--json',
4283 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004284 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004285 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004286 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004287
dnj@chromium.org406c4402015-03-03 17:22:28 +00004288 if options.reverse:
4289 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004290 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004291 # Reverse issue lookup.
4292 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004293
4294 git_config = {}
4295 for config in RunGit(['config', '--get-regexp',
4296 r'branch\..*issue']).splitlines():
4297 name, _space, val = config.partition(' ')
4298 git_config[name] = val
4299
dnj@chromium.org406c4402015-03-03 17:22:28 +00004300 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004301 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4302 config_key = _git_branch_config_key(ShortBranchName(branch),
4303 cls.IssueConfigKey())
4304 issue = git_config.get(config_key)
4305 if issue:
4306 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004307 if not args:
4308 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004309 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004310 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004311 try:
4312 issue_num = int(issue)
4313 except ValueError:
4314 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004315 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004316 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004318 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004319 if options.json:
4320 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004321 return 0
4322
4323 if len(args) > 0:
4324 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4325 if not issue.valid:
4326 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4327 'or no argument to list it.\n'
4328 'Maybe you want to run git cl status?')
4329 cl = Changelist(codereview=issue.codereview)
4330 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004331 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004332 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004333 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4334 if options.json:
4335 write_json(options.json, {
4336 'issue': cl.GetIssue(),
4337 'issue_url': cl.GetIssueURL(),
4338 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339 return 0
4340
4341
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004342@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004343def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004344 """Shows or posts review comments for any changelist."""
4345 parser.add_option('-a', '--add-comment', dest='comment',
4346 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004347 parser.add_option('-p', '--publish', action='store_true',
4348 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004349 parser.add_option('-i', '--issue', dest='issue',
4350 help='review issue id (defaults to current issue). '
4351 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004352 parser.add_option('-m', '--machine-readable', dest='readable',
4353 action='store_false', default=True,
4354 help='output comments in a format compatible with '
4355 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004356 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004357 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004358 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004359 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004360 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004361 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004362 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004363
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004364 issue = None
4365 if options.issue:
4366 try:
4367 issue = int(options.issue)
4368 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004369 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004370
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004371 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4372
4373 if not cl.IsGerrit():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004374 parser.error('Rietveld is not supported.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004375
4376 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004377 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004378 return 0
4379
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004380 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4381 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004382 for comment in summary:
4383 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004384 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004385 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004386 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004387 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004388 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004389 elif comment.autogenerated:
4390 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004391 else:
4392 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004393 print('\n%s%s %s%s\n%s' % (
4394 color,
4395 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4396 comment.sender,
4397 Fore.RESET,
4398 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4399
smut@google.comc85ac942015-09-15 16:34:43 +00004400 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004401 def pre_serialize(c):
4402 dct = c.__dict__.copy()
4403 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4404 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004405 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004406 return 0
4407
4408
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004409@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004410@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004411def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004412 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004413 parser.add_option('-d', '--display', action='store_true',
4414 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004415 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004416 help='New description to set for this issue (- for stdin, '
4417 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004418 parser.add_option('-f', '--force', action='store_true',
4419 help='Delete any unpublished Gerrit edits for this issue '
4420 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004421
4422 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004423 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004424 options, args = parser.parse_args(args)
4425 _process_codereview_select_options(parser, options)
4426
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004427 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004428 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004429 target_issue_arg = ParseIssueNumberArgument(args[0],
4430 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004431 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004432 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004433
martiniss6eda05f2016-06-30 10:18:35 -07004434 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004435 'auth_config': auth.extract_auth_config_from_options(options),
4436 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004437 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004438 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004439 if target_issue_arg:
4440 kwargs['issue'] = target_issue_arg.issue
4441 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004442 if target_issue_arg.codereview and not options.forced_codereview:
4443 detected_codereview_from_url = True
4444 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004445
4446 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004447 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004448 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004449 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004450
4451 if detected_codereview_from_url:
4452 logging.info('canonical issue/change URL: %s (type: %s)\n',
4453 cl.GetIssueURL(), target_issue_arg.codereview)
4454
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004455 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004456
smut@google.com34fb6b12015-07-13 20:03:26 +00004457 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004458 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004459 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004460
4461 if options.new_description:
4462 text = options.new_description
4463 if text == '-':
4464 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004465 elif text == '+':
4466 base_branch = cl.GetCommonAncestorWithUpstream()
4467 change = cl.GetChange(base_branch, None, local_description=True)
4468 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004469
4470 description.set_description(text)
4471 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004472 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004473
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004474 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004475 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004476 return 0
4477
4478
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004479@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004480def CMDlint(parser, args):
4481 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004482 parser.add_option('--filter', action='append', metavar='-x,+y',
4483 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004484 auth.add_auth_options(parser)
4485 options, args = parser.parse_args(args)
4486 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004487
4488 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004489 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004490 try:
4491 import cpplint
4492 import cpplint_chromium
4493 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004494 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004495 return 1
4496
4497 # Change the current working directory before calling lint so that it
4498 # shows the correct base.
4499 previous_cwd = os.getcwd()
4500 os.chdir(settings.GetRoot())
4501 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004502 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004503 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4504 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004505 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004506 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004507 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004508
4509 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004510 command = args + files
4511 if options.filter:
4512 command = ['--filter=' + ','.join(options.filter)] + command
4513 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004514
4515 white_regex = re.compile(settings.GetLintRegex())
4516 black_regex = re.compile(settings.GetLintIgnoreRegex())
4517 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4518 for filename in filenames:
4519 if white_regex.match(filename):
4520 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004521 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004522 else:
4523 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4524 extra_check_functions)
4525 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004526 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004527 finally:
4528 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004529 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004530 if cpplint._cpplint_state.error_count != 0:
4531 return 1
4532 return 0
4533
4534
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004535@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004536def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004537 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004538 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004539 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004540 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004541 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004542 parser.add_option('--all', action='store_true',
4543 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004544 parser.add_option('--parallel', action='store_true',
4545 help='Run all tests specified by input_api.RunTests in all '
4546 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004547 auth.add_auth_options(parser)
4548 options, args = parser.parse_args(args)
4549 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004550
sbc@chromium.org71437c02015-04-09 19:29:40 +00004551 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004552 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004553 return 1
4554
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004555 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556 if args:
4557 base_branch = args[0]
4558 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004559 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004560 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561
Aaron Gable8076c282017-11-29 14:39:41 -08004562 if options.all:
4563 base_change = cl.GetChange(base_branch, None)
4564 files = [('M', f) for f in base_change.AllFiles()]
4565 change = presubmit_support.GitChange(
4566 base_change.Name(),
4567 base_change.FullDescriptionText(),
4568 base_change.RepositoryRoot(),
4569 files,
4570 base_change.issue,
4571 base_change.patchset,
4572 base_change.author_email,
4573 base_change._upstream)
4574 else:
4575 change = cl.GetChange(base_branch, None)
4576
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004577 cl.RunHook(
4578 committing=not options.upload,
4579 may_prompt=False,
4580 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004581 change=change,
4582 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004583 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004584
4585
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004586def GenerateGerritChangeId(message):
4587 """Returns Ixxxxxx...xxx change id.
4588
4589 Works the same way as
4590 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4591 but can be called on demand on all platforms.
4592
4593 The basic idea is to generate git hash of a state of the tree, original commit
4594 message, author/committer info and timestamps.
4595 """
4596 lines = []
4597 tree_hash = RunGitSilent(['write-tree'])
4598 lines.append('tree %s' % tree_hash.strip())
4599 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4600 if code == 0:
4601 lines.append('parent %s' % parent.strip())
4602 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4603 lines.append('author %s' % author.strip())
4604 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4605 lines.append('committer %s' % committer.strip())
4606 lines.append('')
4607 # Note: Gerrit's commit-hook actually cleans message of some lines and
4608 # whitespace. This code is not doing this, but it clearly won't decrease
4609 # entropy.
4610 lines.append(message)
4611 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004612 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004613 return 'I%s' % change_hash.strip()
4614
4615
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004616def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004617 """Computes the remote branch ref to use for the CL.
4618
4619 Args:
4620 remote (str): The git remote for the CL.
4621 remote_branch (str): The git remote branch for the CL.
4622 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004623 """
4624 if not (remote and remote_branch):
4625 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004626
wittman@chromium.org455dc922015-01-26 20:15:50 +00004627 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004628 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004629 # refs, which are then translated into the remote full symbolic refs
4630 # below.
4631 if '/' not in target_branch:
4632 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4633 else:
4634 prefix_replacements = (
4635 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4636 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4637 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4638 )
4639 match = None
4640 for regex, replacement in prefix_replacements:
4641 match = re.search(regex, target_branch)
4642 if match:
4643 remote_branch = target_branch.replace(match.group(0), replacement)
4644 break
4645 if not match:
4646 # This is a branch path but not one we recognize; use as-is.
4647 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004648 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4649 # Handle the refs that need to land in different refs.
4650 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004651
wittman@chromium.org455dc922015-01-26 20:15:50 +00004652 # Create the true path to the remote branch.
4653 # Does the following translation:
4654 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4655 # * refs/remotes/origin/master -> refs/heads/master
4656 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4657 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4658 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4659 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4660 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4661 'refs/heads/')
4662 elif remote_branch.startswith('refs/remotes/branch-heads'):
4663 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004664
wittman@chromium.org455dc922015-01-26 20:15:50 +00004665 return remote_branch
4666
4667
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004668def cleanup_list(l):
4669 """Fixes a list so that comma separated items are put as individual items.
4670
4671 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4672 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4673 """
4674 items = sum((i.split(',') for i in l), [])
4675 stripped_items = (i.strip() for i in items)
4676 return sorted(filter(None, stripped_items))
4677
4678
Aaron Gable4db38df2017-11-03 14:59:07 -07004679@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004680@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004681def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004682 """Uploads the current changelist to codereview.
4683
4684 Can skip dependency patchset uploads for a branch by running:
4685 git config branch.branch_name.skip-deps-uploads True
4686 To unset run:
4687 git config --unset branch.branch_name.skip-deps-uploads
4688 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004689
4690 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4691 a bug number, this bug number is automatically populated in the CL
4692 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004693
4694 If subject contains text in square brackets or has "<text>: " prefix, such
4695 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4696 [git-cl] add support for hashtags
4697 Foo bar: implement foo
4698 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004699 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004700 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4701 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004702 parser.add_option('--bypass-watchlists', action='store_true',
4703 dest='bypass_watchlists',
4704 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004705 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004706 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004707 parser.add_option('--message', '-m', dest='message',
4708 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004709 parser.add_option('-b', '--bug',
4710 help='pre-populate the bug number(s) for this issue. '
4711 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004712 parser.add_option('--message-file', dest='message_file',
4713 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004714 parser.add_option('--title', '-t', dest='title',
4715 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004716 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004717 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004718 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004719 parser.add_option('--tbrs',
4720 action='append', default=[],
4721 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004722 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004723 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004724 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004725 parser.add_option('--hashtag', dest='hashtags',
4726 action='append', default=[],
4727 help=('Gerrit hashtag for new CL; '
4728 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004729 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004730 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004731 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004732 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004733 metavar='TARGET',
4734 help='Apply CL to remote ref TARGET. ' +
4735 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004736 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004737 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004738 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004739 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004740 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004741 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004742 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4743 const='TBR', help='add a set of OWNERS to TBR')
4744 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4745 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004746 parser.add_option('-c', '--use-commit-queue', action='store_true',
4747 help='tell the CQ to commit this patchset; '
4748 'implies --send-mail')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004749 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4750 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004751 help='Send the patchset to do a CQ dry run right after '
4752 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004753 parser.add_option('--preserve-tryjobs', action='store_true',
4754 help='instruct the CQ to let tryjobs running even after '
4755 'new patchsets are uploaded instead of canceling '
4756 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004757 parser.add_option('--dependencies', action='store_true',
4758 help='Uploads CLs of all the local branches that depend on '
4759 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004760 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4761 help='Sends your change to the CQ after an approval. Only '
4762 'works on repos that have the Auto-Submit label '
4763 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004764 parser.add_option('--parallel', action='store_true',
4765 help='Run all tests specified by input_api.RunTests in all '
4766 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004767
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004768 parser.add_option('--no-autocc', action='store_true',
4769 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004770 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004771 help='Set the review private. This implies --no-autocc.')
4772
rmistry@google.com2dd99862015-06-22 12:22:18 +00004773 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004774 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004775 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004776 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004777 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004778 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004779
sbc@chromium.org71437c02015-04-09 19:29:40 +00004780 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004781 return 1
4782
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004783 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004784 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004785 options.cc = cleanup_list(options.cc)
4786
tandriib80458a2016-06-23 12:20:07 -07004787 if options.message_file:
4788 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004789 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004790 options.message = gclient_utils.FileRead(options.message_file)
4791 options.message_file = None
4792
tandrii4d0545a2016-07-06 03:56:49 -07004793 if options.cq_dry_run and options.use_commit_queue:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004794 parser.error('Only one of --use-commit-queue and --cq-dry-run allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004795
Aaron Gableedbc4132017-09-11 13:22:28 -07004796 if options.use_commit_queue:
4797 options.send_mail = True
4798
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004799 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4800 settings.GetIsGerrit()
4801
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004802 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004803 if not cl.IsGerrit():
4804 # Error out with instructions for repos not yet configured for Gerrit.
4805 print('=====================================')
4806 print('NOTICE: Rietveld is no longer supported. '
4807 'You can upload changes to Gerrit with')
4808 print(' git cl upload --gerrit')
4809 print('or set Gerrit to be your default code review tool with')
4810 print(' git config gerrit.host true')
4811 print('=====================================')
4812 return 1
4813
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004814 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004815
4816
Francois Dorayd42c6812017-05-30 15:10:20 -04004817@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004818@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004819def CMDsplit(parser, args):
4820 """Splits a branch into smaller branches and uploads CLs.
4821
4822 Creates a branch and uploads a CL for each group of files modified in the
4823 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004824 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004825 the shared OWNERS file.
4826 """
4827 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004828 help="A text file containing a CL description in which "
4829 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004830 parser.add_option("-c", "--comment", dest="comment_file",
4831 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004832 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4833 default=False,
4834 help="List the files and reviewers for each CL that would "
4835 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004836 parser.add_option("--cq-dry-run", action='store_true',
4837 help="If set, will do a cq dry run for each uploaded CL. "
4838 "Please be careful when doing this; more than ~10 CLs "
4839 "has the potential to overload our build "
4840 "infrastructure. Try to upload these not during high "
4841 "load times (usually 11-3 Mountain View time). Email "
4842 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004843 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4844 default=True,
4845 help='Sends your change to the CQ after an approval. Only '
4846 'works on repos that have the Auto-Submit label '
4847 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004848 options, _ = parser.parse_args(args)
4849
4850 if not options.description_file:
4851 parser.error('No --description flag specified.')
4852
4853 def WrappedCMDupload(args):
4854 return CMDupload(OptionParser(), args)
4855
4856 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004857 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004858 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004859
4860
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004861@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004862@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004863def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004864 """DEPRECATED: Used to commit the current changelist via git-svn."""
4865 message = ('git-cl no longer supports committing to SVN repositories via '
4866 'git-svn. You probably want to use `git cl land` instead.')
4867 print(message)
4868 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004869
4870
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004871# Two special branches used by git cl land.
4872MERGE_BRANCH = 'git-cl-commit'
4873CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4874
4875
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004876@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004877@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004878def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004879 """Commits the current changelist via git.
4880
4881 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4882 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004883 """
4884 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4885 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004886 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004887 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004888 parser.add_option('--parallel', action='store_true',
4889 help='Run all tests specified by input_api.RunTests in all '
4890 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004891 auth.add_auth_options(parser)
4892 (options, args) = parser.parse_args(args)
4893 auth_config = auth.extract_auth_config_from_options(options)
4894
4895 cl = Changelist(auth_config=auth_config)
4896
Robert Iannucci2e73d432018-03-14 01:10:47 -07004897 if not cl.IsGerrit():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004898 parser.error('Rietveld is not supported.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004899
Robert Iannucci2e73d432018-03-14 01:10:47 -07004900 if not cl.GetIssue():
4901 DieWithError('You must upload the change first to Gerrit.\n'
4902 ' If you would rather have `git cl land` upload '
4903 'automatically for you, see http://crbug.com/642759')
4904 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004905 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004906
4907
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004908@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004909@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004910def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004911 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004912 parser.add_option('-b', dest='newbranch',
4913 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004914 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004915 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004916 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004917 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004918 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004919 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004920 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004921 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004922 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004923 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004924
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004925
4926 group = optparse.OptionGroup(
4927 parser,
4928 'Options for continuing work on the current issue uploaded from a '
4929 'different clone (e.g. different machine). Must be used independently '
4930 'from the other options. No issue number should be specified, and the '
4931 'branch must have an issue number associated with it')
4932 group.add_option('--reapply', action='store_true', dest='reapply',
4933 help='Reset the branch and reapply the issue.\n'
4934 'CAUTION: This will undo any local changes in this '
4935 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004936
4937 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004938 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004939 parser.add_option_group(group)
4940
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004941 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004942 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004943 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004944 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004945 auth_config = auth.extract_auth_config_from_options(options)
4946
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004947 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004948 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004949 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004950 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004951 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004952
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004953 cl = Changelist(auth_config=auth_config,
4954 codereview=options.forced_codereview)
4955 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004956 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004957
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004958 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004959 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004960 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004961
4962 RunGit(['reset', '--hard', upstream])
4963 if options.pull:
4964 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004965
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004966 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4967 options.directory)
4968
4969 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004970 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004971
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004972 target_issue_arg = ParseIssueNumberArgument(args[0],
4973 options.forced_codereview)
4974 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004975 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004976
4977 cl_kwargs = {
4978 'auth_config': auth_config,
4979 'codereview_host': target_issue_arg.hostname,
4980 'codereview': options.forced_codereview,
4981 }
4982 detected_codereview_from_url = False
4983 if target_issue_arg.codereview and not options.forced_codereview:
4984 detected_codereview_from_url = True
4985 cl_kwargs['codereview'] = target_issue_arg.codereview
4986 cl_kwargs['issue'] = target_issue_arg.issue
4987
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004988 # We don't want uncommitted changes mixed up with the patch.
4989 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004990 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004991
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004992 if options.newbranch:
4993 if options.force:
4994 RunGit(['branch', '-D', options.newbranch],
4995 stderr=subprocess2.PIPE, error_ok=True)
4996 RunGit(['new-branch', options.newbranch])
4997
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004998 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004999
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005000 if cl.IsGerrit():
5001 if options.reject:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005002 parser.error('--reject is not supported with Gerrit code review.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005003 if options.directory:
5004 parser.error('--directory is not supported with Gerrit codereview.')
5005
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005006 if detected_codereview_from_url:
5007 print('canonical issue/change URL: %s (type: %s)\n' %
5008 (cl.GetIssueURL(), target_issue_arg.codereview))
5009
5010 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005011 options.nocommit, options.directory,
5012 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005013
5014
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005015def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005016 """Fetches the tree status and returns either 'open', 'closed',
5017 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005018 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005019 if url:
5020 status = urllib2.urlopen(url).read().lower()
5021 if status.find('closed') != -1 or status == '0':
5022 return 'closed'
5023 elif status.find('open') != -1 or status == '1':
5024 return 'open'
5025 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005026 return 'unset'
5027
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005029def GetTreeStatusReason():
5030 """Fetches the tree status from a json url and returns the message
5031 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005032 url = settings.GetTreeStatusUrl()
5033 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005034 connection = urllib2.urlopen(json_url)
5035 status = json.loads(connection.read())
5036 connection.close()
5037 return status['message']
5038
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005039
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005040@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005041def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005042 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005043 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005044 status = GetTreeStatus()
5045 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005046 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005047 return 2
5048
vapiera7fbd5a2016-06-16 09:17:49 -07005049 print('The tree is %s' % status)
5050 print()
5051 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005052 if status != 'open':
5053 return 1
5054 return 0
5055
5056
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005057@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005058def CMDtry(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005059 """Triggers tryjobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005060 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005061 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005062 '-b', '--bot', action='append',
5063 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5064 'times to specify multiple builders. ex: '
5065 '"-b win_rel -b win_layout". See '
5066 'the try server waterfall for the builders name and the tests '
5067 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005068 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005069 '-B', '--bucket', default='',
5070 help=('Buildbucket bucket to send the try requests.'))
5071 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005072 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005073 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005074 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005075 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005076 help='Revision to use for the try job; default: the revision will '
5077 'be determined by the try recipe that builder runs, which usually '
5078 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005079 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005080 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005081 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005082 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005083 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005084 '--category', default='git_cl_try', help='Specify custom build category.')
5085 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005086 '--project',
5087 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005088 'in recipe to determine to which repository or directory to '
5089 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005090 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005091 '-p', '--property', dest='properties', action='append', default=[],
5092 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005093 'key2=value2 etc. The value will be treated as '
5094 'json if decodable, or as string otherwise. '
5095 'NOTE: using this may make your try job not usable for CQ, '
5096 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005097 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005098 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5099 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005100 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005101 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005102 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005103 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005104 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005105 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005106
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005107 if options.master and options.master.startswith('luci.'):
5108 parser.error(
5109 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005110 # Make sure that all properties are prop=value pairs.
5111 bad_params = [x for x in options.properties if '=' not in x]
5112 if bad_params:
5113 parser.error('Got properties with missing "=": %s' % bad_params)
5114
maruel@chromium.org15192402012-09-06 12:38:29 +00005115 if args:
5116 parser.error('Unknown arguments: %s' % args)
5117
Koji Ishii31c14782018-01-08 17:17:33 +09005118 cl = Changelist(auth_config=auth_config, issue=options.issue,
5119 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005120 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005121 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005122
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005123 if cl.IsGerrit():
5124 # HACK: warm up Gerrit change detail cache to save on RPCs.
5125 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5126
tandriie113dfd2016-10-11 10:20:12 -07005127 error_message = cl.CannotTriggerTryJobReason()
5128 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005129 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005130
borenet6c0efe62016-10-19 08:13:29 -07005131 if options.bucket and options.master:
5132 parser.error('Only one of --bucket and --master may be used.')
5133
qyearsley1fdfcb62016-10-24 13:22:03 -07005134 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005135
qyearsleydd49f942016-10-28 11:57:22 -07005136 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5137 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005138 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005139 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005140 print('git cl try with no bots now defaults to CQ dry run.')
5141 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5142 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005143
borenet6c0efe62016-10-19 08:13:29 -07005144 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005145 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005146 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005147 'of bot requires an initial job from a parent (usually a builder). '
5148 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005149 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005150 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005151
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005152 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005153 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005154 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005155 except BuildbucketResponseException as ex:
5156 print('ERROR: %s' % ex)
5157 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005158 return 0
5159
5160
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005161@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005162def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005163 """Prints info about results for tryjobs associated with the current CL."""
tandrii1838bad2016-10-06 00:10:52 -07005164 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005165 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005166 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005167 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005168 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005169 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005170 '--color', action='store_true', default=setup_color.IS_TTY,
5171 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005172 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005173 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5174 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005175 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005176 '--json', help=('Path of JSON output file to write try job results to,'
5177 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005178 parser.add_option_group(group)
5179 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005180 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005181 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005182 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005183 if args:
5184 parser.error('Unrecognized args: %s' % ' '.join(args))
5185
5186 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005187 cl = Changelist(
5188 issue=options.issue, codereview=options.forced_codereview,
5189 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005190 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005191 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005192
tandrii221ab252016-10-06 08:12:04 -07005193 patchset = options.patchset
5194 if not patchset:
5195 patchset = cl.GetMostRecentPatchset()
5196 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005197 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005198 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005199 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005200 cl.GetIssue())
5201
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005202 try:
tandrii221ab252016-10-06 08:12:04 -07005203 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005204 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005205 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005206 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005207 if options.json:
5208 write_try_results_json(options.json, jobs)
5209 else:
5210 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005211 return 0
5212
5213
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005214@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005215@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005216def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005217 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005218 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005219 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005220 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005221
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005222 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005223 if args:
5224 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005225 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005226 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005227 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005228 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005229
5230 # Clear configured merge-base, if there is one.
5231 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005232 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005233 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005234 return 0
5235
5236
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005237@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005238def CMDweb(parser, args):
5239 """Opens the current CL in the web browser."""
5240 _, args = parser.parse_args(args)
5241 if args:
5242 parser.error('Unrecognized args: %s' % ' '.join(args))
5243
5244 issue_url = Changelist().GetIssueURL()
5245 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005246 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005247 return 1
5248
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005249 # Redirect I/O before invoking browser to hide its output. For example, this
5250 # allows to hide "Created new window in existing browser session." message
5251 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5252 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005253 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005254 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005255 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005256 os.open(os.devnull, os.O_RDWR)
5257 try:
5258 webbrowser.open(issue_url)
5259 finally:
5260 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005261 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005262 return 0
5263
5264
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005265@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005266def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005267 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005268 parser.add_option('-d', '--dry-run', action='store_true',
5269 help='trigger in dry run mode')
5270 parser.add_option('-c', '--clear', action='store_true',
5271 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005272 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005273 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005274 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005275 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005276 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005277 if args:
5278 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005279 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005280 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005281
iannuccie53c9352016-08-17 14:40:40 -07005282 cl = Changelist(auth_config=auth_config, issue=options.issue,
5283 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005284 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005285 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005286 elif options.dry_run:
5287 state = _CQState.DRY_RUN
5288 else:
5289 state = _CQState.COMMIT
5290 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005291 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07005292 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005293 return 0
5294
5295
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005296@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005297def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005298 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005299 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005300 auth.add_auth_options(parser)
5301 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005302 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005303 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005304 if args:
5305 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005306 cl = Changelist(auth_config=auth_config, issue=options.issue,
5307 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005308 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005309 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005310 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005311 cl.CloseIssue()
5312 return 0
5313
5314
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005315@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005316def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005317 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005318 parser.add_option(
5319 '--stat',
5320 action='store_true',
5321 dest='stat',
5322 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005323 auth.add_auth_options(parser)
5324 options, args = parser.parse_args(args)
5325 auth_config = auth.extract_auth_config_from_options(options)
5326 if args:
5327 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005328
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005329 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005330 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005331 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005332 if not issue:
5333 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005334
Aaron Gablea718c3e2017-08-28 17:47:28 -07005335 base = cl._GitGetBranchConfigValue('last-upload-hash')
5336 if not base:
5337 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5338 if not base:
5339 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5340 revision_info = detail['revisions'][detail['current_revision']]
5341 fetch_info = revision_info['fetch']['http']
5342 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5343 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005344
Aaron Gablea718c3e2017-08-28 17:47:28 -07005345 cmd = ['git', 'diff']
5346 if options.stat:
5347 cmd.append('--stat')
5348 cmd.append(base)
5349 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005350
5351 return 0
5352
5353
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005354@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005355def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005356 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005357 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005358 '--ignore-current',
5359 action='store_true',
5360 help='Ignore the CL\'s current reviewers and start from scratch.')
5361 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005362 '--ignore-self',
5363 action='store_true',
5364 help='Do not consider CL\'s author as an owners.')
5365 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005366 '--no-color',
5367 action='store_true',
5368 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005369 parser.add_option(
5370 '--batch',
5371 action='store_true',
5372 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005373 # TODO: Consider moving this to another command, since other
5374 # git-cl owners commands deal with owners for a given CL.
5375 parser.add_option(
5376 '--show-all',
5377 action='store_true',
5378 help='Show all owners for a particular file')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005379 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005380 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005381 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005382
5383 author = RunGit(['config', 'user.email']).strip() or None
5384
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005385 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005386
Yang Guo6e269a02019-06-26 11:17:02 +00005387 if options.show_all:
5388 for arg in args:
5389 base_branch = cl.GetCommonAncestorWithUpstream()
5390 change = cl.GetChange(base_branch, None)
5391 database = owners.Database(change.RepositoryRoot(), file, os.path)
5392 database.load_data_needed_for([arg])
5393 print('Owners for %s:' % arg)
5394 for owner in sorted(database.all_possible_owners([arg], None)):
5395 print(' - %s' % owner)
5396 return 0
5397
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005398 if args:
5399 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005400 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005401 base_branch = args[0]
5402 else:
5403 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005404 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005405
5406 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005407 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5408
5409 if options.batch:
5410 db = owners.Database(change.RepositoryRoot(), file, os.path)
5411 print('\n'.join(db.reviewers_for(affected_files, author)))
5412 return 0
5413
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005414 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005415 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005416 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005417 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005418 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005419 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005420 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005421 override_files=change.OriginalOwnersFiles(),
5422 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005423
5424
Aiden Bennerc08566e2018-10-03 17:52:42 +00005425def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005426 """Generates a diff command."""
5427 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005428 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5429
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005430 if allow_prefix:
5431 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5432 # case that diff.noprefix is set in the user's git config.
5433 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5434 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005435 diff_cmd += ['--no-prefix']
5436
5437 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005438
5439 if args:
5440 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005441 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005442 diff_cmd.append(arg)
5443 else:
5444 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005445
5446 return diff_cmd
5447
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005448
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005449def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005450 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005451 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005452
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005453
enne@chromium.org555cfe42014-01-29 18:21:39 +00005454@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005455@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005456def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005457 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005458 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005459 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005460 parser.add_option('--full', action='store_true',
5461 help='Reformat the full content of all touched files')
5462 parser.add_option('--dry-run', action='store_true',
5463 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005464 parser.add_option(
5465 '--python',
5466 action='store_true',
5467 default=None,
5468 help='Enables python formatting on all python files.')
5469 parser.add_option(
5470 '--no-python',
5471 action='store_true',
5472 dest='python',
5473 help='Disables python formatting on all python files. '
5474 'Takes precedence over --python. '
5475 'If neither --python or --no-python are set, python '
5476 'files that have a .style.yapf file in an ancestor '
5477 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005478 parser.add_option('--js', action='store_true',
5479 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005480 parser.add_option('--diff', action='store_true',
5481 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005482 parser.add_option('--presubmit', action='store_true',
5483 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005484 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005485
Daniel Chengc55eecf2016-12-30 03:11:02 -08005486 # Normalize any remaining args against the current path, so paths relative to
5487 # the current directory are still resolved as expected.
5488 args = [os.path.join(os.getcwd(), arg) for arg in args]
5489
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005490 # git diff generates paths against the root of the repository. Change
5491 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005492 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005493 if rel_base_path:
5494 os.chdir(rel_base_path)
5495
digit@chromium.org29e47272013-05-17 17:01:46 +00005496 # Grab the merge-base commit, i.e. the upstream commit of the current
5497 # branch when it was created or the last time it was rebased. This is
5498 # to cover the case where the user may have called "git fetch origin",
5499 # moving the origin branch to a newer commit, but hasn't rebased yet.
5500 upstream_commit = None
5501 cl = Changelist()
5502 upstream_branch = cl.GetUpstreamBranch()
5503 if upstream_branch:
5504 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5505 upstream_commit = upstream_commit.strip()
5506
5507 if not upstream_commit:
5508 DieWithError('Could not find base commit for this branch. '
5509 'Are you in detached state?')
5510
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005511 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5512 diff_output = RunGit(changed_files_cmd)
5513 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005514 # Filter out files deleted by this CL
5515 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005516
Christopher Lamc5ba6922017-01-24 11:19:14 +11005517 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005518 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005519
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005520 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5521 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5522 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005523 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005524
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005525 top_dir = os.path.normpath(
5526 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5527
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005528 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5529 # formatted. This is used to block during the presubmit.
5530 return_value = 0
5531
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005532 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005533 # Locate the clang-format binary in the checkout
5534 try:
5535 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005536 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005537 DieWithError(e)
5538
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005539 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005540 cmd = [clang_format_tool]
5541 if not opts.dry_run and not opts.diff:
5542 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005543 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005544 if opts.diff:
5545 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005546 else:
5547 env = os.environ.copy()
5548 env['PATH'] = str(os.path.dirname(clang_format_tool))
5549 try:
5550 script = clang_format.FindClangFormatScriptInChromiumTree(
5551 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005552 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005553 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005554
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005555 cmd = [sys.executable, script, '-p0']
5556 if not opts.dry_run and not opts.diff:
5557 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005558
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005559 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5560 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005561
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005562 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5563 if opts.diff:
5564 sys.stdout.write(stdout)
5565 if opts.dry_run and len(stdout) > 0:
5566 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005567
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005568 # Similar code to above, but using yapf on .py files rather than clang-format
5569 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005570 py_explicitly_disabled = opts.python is not None and not opts.python
5571 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005572 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5573 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5574 if sys.platform.startswith('win'):
5575 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005576
Aiden Bennerc08566e2018-10-03 17:52:42 +00005577 # If we couldn't find a yapf file we'll default to the chromium style
5578 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005579 chromium_default_yapf_style = os.path.join(depot_tools_path,
5580 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005581 # Used for caching.
5582 yapf_configs = {}
5583 for f in python_diff_files:
5584 # Find the yapf style config for the current file, defaults to depot
5585 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005586 _FindYapfConfigFile(f, yapf_configs, top_dir)
5587
5588 # Turn on python formatting by default if a yapf config is specified.
5589 # This breaks in the case of this repo though since the specified
5590 # style file is also the global default.
5591 if opts.python is None:
5592 filtered_py_files = []
5593 for f in python_diff_files:
5594 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5595 filtered_py_files.append(f)
5596 else:
5597 filtered_py_files = python_diff_files
5598
5599 # Note: yapf still seems to fix indentation of the entire file
5600 # even if line ranges are specified.
5601 # See https://github.com/google/yapf/issues/499
5602 if not opts.full and filtered_py_files:
5603 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5604
5605 for f in filtered_py_files:
5606 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5607 if yapf_config is None:
5608 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005609
5610 cmd = [yapf_tool, '--style', yapf_config, f]
5611
5612 has_formattable_lines = False
5613 if not opts.full:
5614 # Only run yapf over changed line ranges.
5615 for diff_start, diff_len in py_line_diffs[f]:
5616 diff_end = diff_start + diff_len - 1
5617 # Yapf errors out if diff_end < diff_start but this
5618 # is a valid line range diff for a removal.
5619 if diff_end >= diff_start:
5620 has_formattable_lines = True
5621 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5622 # If all line diffs were removals we have nothing to format.
5623 if not has_formattable_lines:
5624 continue
5625
5626 if opts.diff or opts.dry_run:
5627 cmd += ['--diff']
5628 # Will return non-zero exit code if non-empty diff.
5629 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5630 if opts.diff:
5631 sys.stdout.write(stdout)
5632 elif len(stdout) > 0:
5633 return_value = 2
5634 else:
5635 cmd += ['-i']
5636 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005637
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005638 # Dart's formatter does not have the nice property of only operating on
5639 # modified chunks, so hard code full.
5640 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005641 try:
5642 command = [dart_format.FindDartFmtToolInChromiumTree()]
5643 if not opts.dry_run and not opts.diff:
5644 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005645 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005646
ppi@chromium.org6593d932016-03-03 15:41:15 +00005647 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005648 if opts.dry_run and stdout:
5649 return_value = 2
5650 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005651 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5652 'found in this checkout. Files in other languages are still '
5653 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005654
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005655 # Format GN build files. Always run on full build files for canonical form.
5656 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005657 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005658 if opts.dry_run or opts.diff:
5659 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005660 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005661 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5662 shell=sys.platform == 'win32',
5663 cwd=top_dir)
5664 if opts.dry_run and gn_ret == 2:
5665 return_value = 2 # Not formatted.
5666 elif opts.diff and gn_ret == 2:
5667 # TODO this should compute and print the actual diff.
5668 print("This change has GN build file diff for " + gn_diff_file)
5669 elif gn_ret != 0:
5670 # For non-dry run cases (and non-2 return values for dry-run), a
5671 # nonzero error code indicates a failure, probably because the file
5672 # doesn't parse.
5673 DieWithError("gn format failed on " + gn_diff_file +
5674 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005675
Ilya Shermane081cbe2017-08-15 17:51:04 -07005676 # Skip the metrics formatting from the global presubmit hook. These files have
5677 # a separate presubmit hook that issues an error if the files need formatting,
5678 # whereas the top-level presubmit script merely issues a warning. Formatting
5679 # these files is somewhat slow, so it's important not to duplicate the work.
5680 if not opts.presubmit:
5681 for xml_dir in GetDirtyMetricsDirs(diff_files):
5682 tool_dir = os.path.join(top_dir, xml_dir)
5683 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5684 if opts.dry_run or opts.diff:
5685 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005686 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005687 if opts.diff:
5688 sys.stdout.write(stdout)
5689 if opts.dry_run and stdout:
5690 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005691
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005692 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005693
Steven Holte2e664bf2017-04-21 13:10:47 -07005694def GetDirtyMetricsDirs(diff_files):
5695 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5696 metrics_xml_dirs = [
5697 os.path.join('tools', 'metrics', 'actions'),
5698 os.path.join('tools', 'metrics', 'histograms'),
5699 os.path.join('tools', 'metrics', 'rappor'),
5700 os.path.join('tools', 'metrics', 'ukm')]
5701 for xml_dir in metrics_xml_dirs:
5702 if any(file.startswith(xml_dir) for file in xml_diff_files):
5703 yield xml_dir
5704
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005705
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005706@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005707@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005708def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005709 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005710 _, args = parser.parse_args(args)
5711
5712 if len(args) != 1:
5713 parser.print_help()
5714 return 1
5715
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005716 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005717 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005718 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005719
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005720 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005721
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005722 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005723 output = RunGit(['config', '--local', '--get-regexp',
5724 r'branch\..*\.%s' % issueprefix],
5725 error_ok=True)
5726 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005727 if issue == target_issue:
5728 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005729
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005730 branches = []
5731 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005732 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005733 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005734 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005735 return 1
5736 if len(branches) == 1:
5737 RunGit(['checkout', branches[0]])
5738 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005739 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005740 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005741 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005742 which = raw_input('Choose by index: ')
5743 try:
5744 RunGit(['checkout', branches[int(which)]])
5745 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005746 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005747 return 1
5748
5749 return 0
5750
5751
maruel@chromium.org29404b52014-09-08 22:58:00 +00005752def CMDlol(parser, args):
5753 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005754 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005755 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5756 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5757 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005758 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005759 return 0
5760
5761
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005762class OptionParser(optparse.OptionParser):
5763 """Creates the option parse and add --verbose support."""
5764 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005765 optparse.OptionParser.__init__(
5766 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005767 self.add_option(
5768 '-v', '--verbose', action='count', default=0,
5769 help='Use 2 times for more debugging info')
5770
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005771 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005772 try:
5773 return self._parse_args(args)
5774 finally:
5775 # Regardless of success or failure of args parsing, we want to report
5776 # metrics, but only after logging has been initialized (if parsing
5777 # succeeded).
5778 global settings
5779 settings = Settings()
5780
5781 if not metrics.DISABLE_METRICS_COLLECTION:
5782 # GetViewVCUrl ultimately calls logging method.
5783 project_url = settings.GetViewVCUrl().strip('/+')
5784 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5785 metrics.collector.add('project_urls', [project_url])
5786
5787 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005788 # Create an optparse.Values object that will store only the actual passed
5789 # options, without the defaults.
5790 actual_options = optparse.Values()
5791 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5792 # Create an optparse.Values object with the default options.
5793 options = optparse.Values(self.get_default_values().__dict__)
5794 # Update it with the options passed by the user.
5795 options._update_careful(actual_options.__dict__)
5796 # Store the options passed by the user in an _actual_options attribute.
5797 # We store only the keys, and not the values, since the values can contain
5798 # arbitrary information, which might be PII.
5799 metrics.collector.add('arguments', actual_options.__dict__.keys())
5800
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005801 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005802 logging.basicConfig(
5803 level=levels[min(options.verbose, len(levels) - 1)],
5804 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5805 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005806
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005807 return options, args
5808
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005810def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005811 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005812 print('\nYour python version %s is unsupported, please upgrade.\n' %
5813 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005814 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005815
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005816 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005817 dispatcher = subcommand.CommandDispatcher(__name__)
5818 try:
5819 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005820 except auth.AuthenticationError as e:
5821 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005822 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005823 if e.code != 500:
5824 raise
5825 DieWithError(
5826 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005827 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005828 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005829
5830
5831if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005832 # These affect sys.stdout so do it outside of main() to simplify mocks in
5833 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005834 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005835 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005836 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005837 sys.exit(main(sys.argv[1:]))