blob: 8f82be47b7999e7476086e47bf55e76e4aae8cae [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +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
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
Edward Lemur0f58ae42019-04-30 17:24:12 +000071# Traces for git push will be stored in a traces directory inside the
72# depot_tools checkout.
73DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
74TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
75
76# When collecting traces, Git hashes will be reduced to 6 characters to reduce
77# the size after compression.
78GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
79# Used to redact the cookies from the gitcookies file.
80GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
81
82TRACES_MESSAGE = (
83'When filing a bug, be sure to include the traces found at:\n'
84' %s.zip\n'
85'Consider including the git config and gitcookies,\n'
86'which we have packed for you at:\n'
87' %s.zip\n')
88
tandrii9d2c7a32016-06-22 03:42:45 -070089COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080090POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000092REFS_THAT_ALIAS_TO_OTHER_REFS = {
93 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
94 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
95}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
thestig@chromium.org44202a22014-03-11 19:22:18 +000097# Valid extensions for files we want to lint.
98DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
99DEFAULT_LINT_IGNORE_REGEX = r"$^"
100
Aiden Bennerc08566e2018-10-03 17:52:42 +0000101# File name for yapf style config files.
102YAPF_CONFIG_FILENAME = '.style.yapf'
103
borenet6c0efe62016-10-19 08:13:29 -0700104# Buildbucket master name prefix.
105MASTER_PREFIX = 'master.'
106
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000107# Shortcut since it quickly becomes redundant.
108Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000109
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000110# Initialized in main()
111settings = None
112
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100113# Used by tests/git_cl_test.py to add extra logging.
114# Inside the weirdly failing test, add this:
115# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700116# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100117_IS_BEING_TESTED = False
118
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000119
Christopher Lamf732cd52017-01-24 12:40:11 +1100120def DieWithError(message, change_desc=None):
121 if change_desc:
122 SaveDescriptionBackup(change_desc)
123
vapiera7fbd5a2016-06-16 09:17:49 -0700124 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000125 sys.exit(1)
126
127
Christopher Lamf732cd52017-01-24 12:40:11 +1100128def SaveDescriptionBackup(change_desc):
129 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000130 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100131 backup_file = open(backup_path, 'w')
132 backup_file.write(change_desc.description)
133 backup_file.close()
134
135
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000136def GetNoGitPagerEnv():
137 env = os.environ.copy()
138 # 'cat' is a magical git string that disables pagers on all platforms.
139 env['GIT_PAGER'] = 'cat'
140 return env
141
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000142
bsep@chromium.org627d9002016-04-29 00:00:52 +0000143def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000144 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000145 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000146 except subprocess2.CalledProcessError as e:
147 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000148 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000149 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000150 'Command "%s" failed.\n%s' % (
151 ' '.join(args), error_message or e.stdout or ''))
152 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153
154
155def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000156 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000157 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158
159
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000160def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000161 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700162 if suppress_stderr:
163 stderr = subprocess2.VOID
164 else:
165 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000166 try:
tandrii5d48c322016-08-18 16:19:37 -0700167 (out, _), code = subprocess2.communicate(['git'] + args,
168 env=GetNoGitPagerEnv(),
169 stdout=subprocess2.PIPE,
170 stderr=stderr)
171 return code, out
172 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900173 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700174 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000175
176
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000177def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000178 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000179 return RunGitWithCode(args, suppress_stderr=True)[1]
180
181
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000182def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000183 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000184 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000185 return (version.startswith(prefix) and
186 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000187
188
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000189def BranchExists(branch):
190 """Return True if specified branch exists."""
191 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
192 suppress_stderr=True)
193 return not code
194
195
tandrii2a16b952016-10-19 07:09:44 -0700196def time_sleep(seconds):
197 # Use this so that it can be mocked in tests without interfering with python
198 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700199 return time.sleep(seconds)
200
201
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000202def time_time():
203 # Use this so that it can be mocked in tests without interfering with python
204 # system machinery.
205 return time.time()
206
207
maruel@chromium.org90541732011-04-01 17:54:18 +0000208def ask_for_data(prompt):
209 try:
210 return raw_input(prompt)
211 except KeyboardInterrupt:
212 # Hide the exception.
213 sys.exit(1)
214
215
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100216def confirm_or_exit(prefix='', action='confirm'):
217 """Asks user to press enter to continue or press Ctrl+C to abort."""
218 if not prefix or prefix.endswith('\n'):
219 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100220 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100221 mid = ' Press'
222 elif prefix.endswith(' '):
223 mid = 'press'
224 else:
225 mid = ' press'
226 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
227
228
229def ask_for_explicit_yes(prompt):
230 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
231 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
232 while True:
233 if 'yes'.startswith(result):
234 return True
235 if 'no'.startswith(result):
236 return False
237 result = ask_for_data('Please, type yes or no: ').lower()
238
239
tandrii5d48c322016-08-18 16:19:37 -0700240def _git_branch_config_key(branch, key):
241 """Helper method to return Git config key for a branch."""
242 assert branch, 'branch name is required to set git config for it'
243 return 'branch.%s.%s' % (branch, key)
244
245
246def _git_get_branch_config_value(key, default=None, value_type=str,
247 branch=False):
248 """Returns git config value of given or current branch if any.
249
250 Returns default in all other cases.
251 """
252 assert value_type in (int, str, bool)
253 if branch is False: # Distinguishing default arg value from None.
254 branch = GetCurrentBranch()
255
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000256 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700257 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000258
tandrii5d48c322016-08-18 16:19:37 -0700259 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700260 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700261 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700262 # git config also has --int, but apparently git config suffers from integer
263 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700264 args.append(_git_branch_config_key(branch, key))
265 code, out = RunGitWithCode(args)
266 if code == 0:
267 value = out.strip()
268 if value_type == int:
269 return int(value)
270 if value_type == bool:
271 return bool(value.lower() == 'true')
272 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000273 return default
274
275
tandrii5d48c322016-08-18 16:19:37 -0700276def _git_set_branch_config_value(key, value, branch=None, **kwargs):
277 """Sets the value or unsets if it's None of a git branch config.
278
279 Valid, though not necessarily existing, branch must be provided,
280 otherwise currently checked out branch is used.
281 """
282 if not branch:
283 branch = GetCurrentBranch()
284 assert branch, 'a branch name OR currently checked out branch is required'
285 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700286 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700287 if value is None:
288 args.append('--unset')
289 elif isinstance(value, bool):
290 args.append('--bool')
291 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700292 else:
tandrii33a46ff2016-08-23 05:53:40 -0700293 # git config also has --int, but apparently git config suffers from integer
294 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700295 value = str(value)
296 args.append(_git_branch_config_key(branch, key))
297 if value is not None:
298 args.append(value)
299 RunGit(args, **kwargs)
300
301
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100302def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700303 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100304
305 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
306 """
307 # Git also stores timezone offset, but it only affects visual display,
308 # actual point in time is defined by this timestamp only.
309 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
310
311
312def _git_amend_head(message, committer_timestamp):
313 """Amends commit with new message and desired committer_timestamp.
314
315 Sets committer timezone to UTC.
316 """
317 env = os.environ.copy()
318 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
319 return RunGit(['commit', '--amend', '-m', message], env=env)
320
321
machenbach@chromium.org45453142015-09-15 08:45:22 +0000322def _get_properties_from_options(options):
323 properties = dict(x.split('=', 1) for x in options.properties)
324 for key, val in properties.iteritems():
325 try:
326 properties[key] = json.loads(val)
327 except ValueError:
328 pass # If a value couldn't be evaluated, treat it as a string.
329 return properties
330
331
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332def _prefix_master(master):
333 """Convert user-specified master name to full master name.
334
335 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
336 name, while the developers always use shortened master name
337 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
338 function does the conversion for buildbucket migration.
339 """
borenet6c0efe62016-10-19 08:13:29 -0700340 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000341 return master
borenet6c0efe62016-10-19 08:13:29 -0700342 return '%s%s' % (MASTER_PREFIX, master)
343
344
345def _unprefix_master(bucket):
346 """Convert bucket name to shortened master name.
347
348 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
349 name, while the developers always use shortened master name
350 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
351 function does the conversion for buildbucket migration.
352 """
353 if bucket.startswith(MASTER_PREFIX):
354 return bucket[len(MASTER_PREFIX):]
355 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000356
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358def _buildbucket_retry(operation_name, http, *args, **kwargs):
359 """Retries requests to buildbucket service and returns parsed json content."""
360 try_count = 0
361 while True:
362 response, content = http.request(*args, **kwargs)
363 try:
364 content_json = json.loads(content)
365 except ValueError:
366 content_json = None
367
368 # Buildbucket could return an error even if status==200.
369 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000370 error = content_json.get('error')
371 if error.get('code') == 403:
372 raise BuildbucketResponseException(
373 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000375 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000376 raise BuildbucketResponseException(msg)
377
378 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700379 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000380 raise BuildbucketResponseException(
381 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700382 'Please file bugs at http://crbug.com, '
383 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000384 content)
385 return content_json
386 if response.status < 500 or try_count >= 2:
387 raise httplib2.HttpLib2Error(content)
388
389 # status >= 500 means transient failures.
390 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700391 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000392 try_count += 1
393 assert False, 'unreachable'
394
395
qyearsley1fdfcb62016-10-24 13:22:03 -0700396def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700397 """Returns a dict mapping bucket names to builders and tests,
398 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700399 """
qyearsleydd49f942016-10-28 11:57:22 -0700400 # If no bots are listed, we try to get a set of builders and tests based
401 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700402 if not options.bot:
403 change = changelist.GetChange(
404 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700405 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700406 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 change=change,
408 changed_files=change.LocalPaths(),
409 repository_root=settings.GetRoot(),
410 default_presubmit=None,
411 project=None,
412 verbose=options.verbose,
413 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700414 if masters is None:
415 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100416 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700417
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 if options.bucket:
419 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700420 if options.master:
421 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700422
qyearsleydd49f942016-10-28 11:57:22 -0700423 # If bots are listed but no master or bucket, then we need to find out
424 # the corresponding master for each bot.
425 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
426 if error_message:
427 option_parser.error(
428 'Tryserver master cannot be found because: %s\n'
429 'Please manually specify the tryserver master, e.g. '
430 '"-m tryserver.chromium.linux".' % error_message)
431 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
433
qyearsley123a4682016-10-26 09:12:17 -0700434def _get_bucket_map_for_builders(builders):
435 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700436 map_url = 'https://builders-map.appspot.com/'
437 try:
qyearsley123a4682016-10-26 09:12:17 -0700438 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700439 except urllib2.URLError as e:
440 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
441 (map_url, e))
442 except ValueError as e:
443 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700444 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700445 return None, 'Failed to build master map.'
446
qyearsley123a4682016-10-26 09:12:17 -0700447 bucket_map = {}
448 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800449 bucket = builders_map.get(builder, {}).get('bucket')
450 if bucket:
451 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700452 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700453
454
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800455def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700456 """Sends a request to Buildbucket to trigger try jobs for a changelist.
457
458 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700459 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700460 changelist: Changelist that the try jobs are associated with.
461 buckets: A nested dict mapping bucket names to builders to tests.
462 options: Command-line options.
463 """
tandriide281ae2016-10-12 06:02:30 -0700464 assert changelist.GetIssue(), 'CL must be uploaded first'
465 codereview_url = changelist.GetCodereviewServer()
466 assert codereview_url, 'CL must be uploaded first'
467 patchset = patchset or changelist.GetMostRecentPatchset()
468 assert patchset, 'CL must be uploaded first'
469
470 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700471 # Cache the buildbucket credentials under the codereview host key, so that
472 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700473 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474 http = authenticator.authorize(httplib2.Http())
475 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700476
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477 buildbucket_put_url = (
478 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000479 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000480 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700481 hostname=codereview_host,
482 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700484
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700485 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800486 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700487 if options.clobber:
488 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700489 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700490 if extra_properties:
491 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492
493 batch_req_body = {'builds': []}
494 print_text = []
495 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700496 for bucket, builders_and_tests in sorted(buckets.iteritems()):
497 print_text.append('Bucket: %s' % bucket)
498 master = None
499 if bucket.startswith(MASTER_PREFIX):
500 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000501 for builder, tests in sorted(builders_and_tests.iteritems()):
502 print_text.append(' %s: %s' % (builder, tests))
503 parameters = {
504 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000505 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100506 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000507 'revision': options.revision,
508 }],
tandrii8c5a3532016-11-04 07:52:02 -0700509 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000511 if 'presubmit' in builder.lower():
512 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000513 if tests:
514 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700515
516 tags = [
517 'builder:%s' % builder,
518 'buildset:%s' % buildset,
519 'user_agent:git_cl_try',
520 ]
521 if master:
522 parameters['properties']['master'] = master
523 tags.append('master:%s' % master)
524
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525 batch_req_body['builds'].append(
526 {
527 'bucket': bucket,
528 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000529 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700530 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000531 }
532 )
533
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700535 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536 http,
537 buildbucket_put_url,
538 'PUT',
539 body=json.dumps(batch_req_body),
540 headers={'Content-Type': 'application/json'}
541 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000542 print_text.append('To see results here, run: git cl try-results')
543 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700544 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000545
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000546
tandrii221ab252016-10-06 08:12:04 -0700547def fetch_try_jobs(auth_config, changelist, buildbucket_host,
548 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700549 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550
qyearsley53f48a12016-09-01 10:45:13 -0700551 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 """
tandrii221ab252016-10-06 08:12:04 -0700553 assert buildbucket_host
554 assert changelist.GetIssue(), 'CL must be uploaded first'
555 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
556 patchset = patchset or changelist.GetMostRecentPatchset()
557 assert patchset, 'CL must be uploaded first'
558
559 codereview_url = changelist.GetCodereviewServer()
560 codereview_host = urlparse.urlparse(codereview_url).hostname
561 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 if authenticator.has_cached_credentials():
563 http = authenticator.authorize(httplib2.Http())
564 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700565 print('Warning: Some results might be missing because %s' %
566 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700567 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 http = httplib2.Http()
569
570 http.force_exception_to_status_code = True
571
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000572 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700573 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700575 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 params = {'tag': 'buildset:%s' % buildset}
577
578 builds = {}
579 while True:
580 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700581 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700583 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584 for build in content.get('builds', []):
585 builds[build['id']] = build
586 if 'next_cursor' in content:
587 params['start_cursor'] = content['next_cursor']
588 else:
589 break
590 return builds
591
592
qyearsleyeab3c042016-08-24 09:18:28 -0700593def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594 """Prints nicely result of fetch_try_jobs."""
595 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700596 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 return
598
599 # Make a copy, because we'll be modifying builds dictionary.
600 builds = builds.copy()
601 builder_names_cache = {}
602
603 def get_builder(b):
604 try:
605 return builder_names_cache[b['id']]
606 except KeyError:
607 try:
608 parameters = json.loads(b['parameters_json'])
609 name = parameters['builder_name']
610 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700611 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700612 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000613 name = None
614 builder_names_cache[b['id']] = name
615 return name
616
617 def get_bucket(b):
618 bucket = b['bucket']
619 if bucket.startswith('master.'):
620 return bucket[len('master.'):]
621 return bucket
622
623 if options.print_master:
624 name_fmt = '%%-%ds %%-%ds' % (
625 max(len(str(get_bucket(b))) for b in builds.itervalues()),
626 max(len(str(get_builder(b))) for b in builds.itervalues()))
627 def get_name(b):
628 return name_fmt % (get_bucket(b), get_builder(b))
629 else:
630 name_fmt = '%%-%ds' % (
631 max(len(str(get_builder(b))) for b in builds.itervalues()))
632 def get_name(b):
633 return name_fmt % get_builder(b)
634
635 def sort_key(b):
636 return b['status'], b.get('result'), get_name(b), b.get('url')
637
638 def pop(title, f, color=None, **kwargs):
639 """Pop matching builds from `builds` dict and print them."""
640
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000641 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000642 colorize = str
643 else:
644 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
645
646 result = []
647 for b in builds.values():
648 if all(b.get(k) == v for k, v in kwargs.iteritems()):
649 builds.pop(b['id'])
650 result.append(b)
651 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700652 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000653 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700654 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000655
656 total = len(builds)
657 pop(status='COMPLETED', result='SUCCESS',
658 title='Successes:', color=Fore.GREEN,
659 f=lambda b: (get_name(b), b.get('url')))
660 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
661 title='Infra Failures:', color=Fore.MAGENTA,
662 f=lambda b: (get_name(b), b.get('url')))
663 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
664 title='Failures:', color=Fore.RED,
665 f=lambda b: (get_name(b), b.get('url')))
666 pop(status='COMPLETED', result='CANCELED',
667 title='Canceled:', color=Fore.MAGENTA,
668 f=lambda b: (get_name(b),))
669 pop(status='COMPLETED', result='FAILURE',
670 failure_reason='INVALID_BUILD_DEFINITION',
671 title='Wrong master/builder name:', color=Fore.MAGENTA,
672 f=lambda b: (get_name(b),))
673 pop(status='COMPLETED', result='FAILURE',
674 title='Other failures:',
675 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
676 pop(status='COMPLETED',
677 title='Other finished:',
678 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
679 pop(status='STARTED',
680 title='Started:', color=Fore.YELLOW,
681 f=lambda b: (get_name(b), b.get('url')))
682 pop(status='SCHEDULED',
683 title='Scheduled:',
684 f=lambda b: (get_name(b), 'id=%s' % b['id']))
685 # The last section is just in case buildbucket API changes OR there is a bug.
686 pop(title='Other:',
687 f=lambda b: (get_name(b), 'id=%s' % b['id']))
688 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700689 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000690
691
Aiden Bennerc08566e2018-10-03 17:52:42 +0000692def _ComputeDiffLineRanges(files, upstream_commit):
693 """Gets the changed line ranges for each file since upstream_commit.
694
695 Parses a git diff on provided files and returns a dict that maps a file name
696 to an ordered list of range tuples in the form (start_line, count).
697 Ranges are in the same format as a git diff.
698 """
699 # If files is empty then diff_output will be a full diff.
700 if len(files) == 0:
701 return {}
702
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000703 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000704 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
705 diff_output = RunGit(diff_cmd)
706
707 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
708 # 2 capture groups
709 # 0 == fname of diff file
710 # 1 == 'diff_start,diff_count' or 'diff_start'
711 # will match each of
712 # diff --git a/foo.foo b/foo.py
713 # @@ -12,2 +14,3 @@
714 # @@ -12,2 +17 @@
715 # running re.findall on the above string with pattern will give
716 # [('foo.py', ''), ('', '14,3'), ('', '17')]
717
718 curr_file = None
719 line_diffs = {}
720 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
721 if match[0] != '':
722 # Will match the second filename in diff --git a/a.py b/b.py.
723 curr_file = match[0]
724 line_diffs[curr_file] = []
725 else:
726 # Matches +14,3
727 if ',' in match[1]:
728 diff_start, diff_count = match[1].split(',')
729 else:
730 # Single line changes are of the form +12 instead of +12,1.
731 diff_start = match[1]
732 diff_count = 1
733
734 diff_start = int(diff_start)
735 diff_count = int(diff_count)
736
737 # If diff_count == 0 this is a removal we can ignore.
738 line_diffs[curr_file].append((diff_start, diff_count))
739
740 return line_diffs
741
742
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000743def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000744 """Checks if a yapf file is in any parent directory of fpath until top_dir.
745
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000746 Recursively checks parent directories to find yapf file and if no yapf file
747 is found returns None. Uses yapf_config_cache as a cache for
748 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000749 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000750 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000751 # Return result if we've already computed it.
752 if fpath in yapf_config_cache:
753 return yapf_config_cache[fpath]
754
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000755 parent_dir = os.path.dirname(fpath)
756 if os.path.isfile(fpath):
757 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000758 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000759 # Otherwise fpath is a directory
760 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
761 if os.path.isfile(yapf_file):
762 ret = yapf_file
763 elif fpath == top_dir or parent_dir == fpath:
764 # If we're at the top level directory, or if we're at root
765 # there is no provided style.
766 ret = None
767 else:
768 # Otherwise recurse on the current directory.
769 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000770 yapf_config_cache[fpath] = ret
771 return ret
772
773
qyearsley53f48a12016-09-01 10:45:13 -0700774def write_try_results_json(output_file, builds):
775 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
776
777 The input |builds| dict is assumed to be generated by Buildbucket.
778 Buildbucket documentation: http://goo.gl/G0s101
779 """
780
781 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800782 """Extracts some of the information from one build dict."""
783 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700784 return {
785 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700786 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800787 'builder_name': parameters.get('builder_name'),
788 'created_ts': build.get('created_ts'),
789 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700790 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800791 'result': build.get('result'),
792 'status': build.get('status'),
793 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700794 'url': build.get('url'),
795 }
796
797 converted = []
798 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000799 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700800 write_json(output_file, converted)
801
802
Aaron Gable13101a62018-02-09 13:20:41 -0800803def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000804 """Prints statistics about the change to the user."""
805 # --no-ext-diff is broken in some versions of Git, so try to work around
806 # this by overriding the environment (but there is still a problem if the
807 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000809 if 'GIT_EXTERNAL_DIFF' in env:
810 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000811
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000812 try:
813 stdout = sys.stdout.fileno()
814 except AttributeError:
815 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000816 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800817 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000818 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000819
820
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000821class BuildbucketResponseException(Exception):
822 pass
823
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825class Settings(object):
826 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000828 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829 self.tree_status_url = None
830 self.viewvc_url = None
831 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000832 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000834 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000835 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837 def LazyUpdateIfNeeded(self):
838 """Updates the settings from a codereview.settings file, if available."""
839 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000840 # The only value that actually changes the behavior is
841 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000842 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000843 error_ok=True
844 ).strip().lower()
845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000847 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 LoadCodereviewSettingsFromFile(cr_settings_file)
849 self.updated = True
850
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000851 @staticmethod
852 def GetRelativeRoot():
853 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000856 if self.root is None:
857 self.root = os.path.abspath(self.GetRelativeRoot())
858 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 def GetTreeStatusUrl(self, error_ok=False):
861 if not self.tree_status_url:
862 error_message = ('You must configure your tree status URL by running '
863 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000864 self.tree_status_url = self._GetConfig(
865 'rietveld.tree-status-url', error_ok=error_ok,
866 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 return self.tree_status_url
868
869 def GetViewVCUrl(self):
870 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000871 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 return self.viewvc_url
873
rmistry@google.com90752582014-01-14 21:04:50 +0000874 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000875 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000876
rmistry@google.com5626a922015-02-26 14:03:30 +0000877 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000878 run_post_upload_hook = self._GetConfig(
879 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000880 return run_post_upload_hook == "True"
881
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000882 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000883 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000884
ukai@chromium.orge8077812012-02-03 03:41:46 +0000885 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700886 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000887 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700888 self.is_gerrit = (
889 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000890 return self.is_gerrit
891
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000892 def GetSquashGerritUploads(self):
893 """Return true if uploads to Gerrit should be squashed by default."""
894 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700895 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
896 if self.squash_gerrit_uploads is None:
897 # Default is squash now (http://crbug.com/611892#c23).
898 self.squash_gerrit_uploads = not (
899 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
900 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000901 return self.squash_gerrit_uploads
902
tandriia60502f2016-06-20 02:01:53 -0700903 def GetSquashGerritUploadsOverride(self):
904 """Return True or False if codereview.settings should be overridden.
905
906 Returns None if no override has been defined.
907 """
908 # See also http://crbug.com/611892#c23
909 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
910 error_ok=True).strip()
911 if result == 'true':
912 return True
913 if result == 'false':
914 return False
915 return None
916
tandrii@chromium.org28253532016-04-14 13:46:56 +0000917 def GetGerritSkipEnsureAuthenticated(self):
918 """Return True if EnsureAuthenticated should not be done for Gerrit
919 uploads."""
920 if self.gerrit_skip_ensure_authenticated is None:
921 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000922 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000923 error_ok=True).strip() == 'true')
924 return self.gerrit_skip_ensure_authenticated
925
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000926 def GetGitEditor(self):
927 """Return the editor specified in the git config, or None if none is."""
928 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000929 # Git requires single quotes for paths with spaces. We need to replace
930 # them with double quotes for Windows to treat such paths as a single
931 # path.
932 self.git_editor = self._GetConfig(
933 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000934 return self.git_editor or None
935
thestig@chromium.org44202a22014-03-11 19:22:18 +0000936 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000937 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000938 DEFAULT_LINT_REGEX)
939
940 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000941 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000942 DEFAULT_LINT_IGNORE_REGEX)
943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944 def _GetConfig(self, param, **kwargs):
945 self.LazyUpdateIfNeeded()
946 return RunGit(['config', param], **kwargs).strip()
947
948
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100949@contextlib.contextmanager
950def _get_gerrit_project_config_file(remote_url):
951 """Context manager to fetch and store Gerrit's project.config from
952 refs/meta/config branch and store it in temp file.
953
954 Provides a temporary filename or None if there was error.
955 """
956 error, _ = RunGitWithCode([
957 'fetch', remote_url,
958 '+refs/meta/config:refs/git_cl/meta/config'])
959 if error:
960 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700961 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100962 (remote_url, error))
963 yield None
964 return
965
966 error, project_config_data = RunGitWithCode(
967 ['show', 'refs/git_cl/meta/config:project.config'])
968 if error:
969 print('WARNING: project.config file not found')
970 yield None
971 return
972
973 with gclient_utils.temporary_directory() as tempdir:
974 project_config_file = os.path.join(tempdir, 'project.config')
975 gclient_utils.FileWrite(project_config_file, project_config_data)
976 yield project_config_file
977
978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979def ShortBranchName(branch):
980 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000981 return branch.replace('refs/heads/', '', 1)
982
983
984def GetCurrentBranchRef():
985 """Returns branch ref (e.g., refs/heads/master) or None."""
986 return RunGit(['symbolic-ref', 'HEAD'],
987 stderr=subprocess2.VOID, error_ok=True).strip() or None
988
989
990def GetCurrentBranch():
991 """Returns current branch or None.
992
993 For refs/heads/* branches, returns just last part. For others, full ref.
994 """
995 branchref = GetCurrentBranchRef()
996 if branchref:
997 return ShortBranchName(branchref)
998 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999
1000
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001001class _CQState(object):
1002 """Enum for states of CL with respect to Commit Queue."""
1003 NONE = 'none'
1004 DRY_RUN = 'dry_run'
1005 COMMIT = 'commit'
1006
1007 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1008
1009
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001010class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001011 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012 self.issue = issue
1013 self.patchset = patchset
1014 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001015 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001016 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001017
1018 @property
1019 def valid(self):
1020 return self.issue is not None
1021
1022
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001023def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001024 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1025 fail_result = _ParsedIssueNumberArgument()
1026
1027 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001028 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001029 if not arg.startswith('http'):
1030 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001031
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001032 url = gclient_utils.UpgradeToHttps(arg)
1033 try:
1034 parsed_url = urlparse.urlparse(url)
1035 except ValueError:
1036 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001037
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001038 if codereview is not None:
1039 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1040 return parsed or fail_result
1041
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001042 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043
1044
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001045def _create_description_from_log(args):
1046 """Pulls out the commit log to use as a base for the CL description."""
1047 log_args = []
1048 if len(args) == 1 and not args[0].endswith('.'):
1049 log_args = [args[0] + '..']
1050 elif len(args) == 1 and args[0].endswith('...'):
1051 log_args = [args[0][:-1]]
1052 elif len(args) == 2:
1053 log_args = [args[0] + '..' + args[1]]
1054 else:
1055 log_args = args[:] # Hope for the best!
1056 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1057
1058
Aaron Gablea45ee112016-11-22 15:14:38 -08001059class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001060 def __init__(self, issue, url):
1061 self.issue = issue
1062 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001063 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001064
1065 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001066 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001067 self.issue, self.url)
1068
1069
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001070_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001071 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001072 # TODO(tandrii): these two aren't known in Gerrit.
1073 'approval', 'disapproval'])
1074
1075
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001077 """Changelist works with one changelist in local branch.
1078
1079 Supports two codereview backends: Rietveld or Gerrit, selected at object
1080 creation.
1081
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001082 Notes:
1083 * Not safe for concurrent multi-{thread,process} use.
1084 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001085 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001086 """
1087
1088 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1089 """Create a new ChangeList instance.
1090
1091 If issue is given, the codereview must be given too.
1092
1093 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1094 Otherwise, it's decided based on current configuration of the local branch,
1095 with default being 'rietveld' for backwards compatibility.
1096 See _load_codereview_impl for more details.
1097
1098 **kwargs will be passed directly to codereview implementation.
1099 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001101 global settings
1102 if not settings:
1103 # Happens when git_cl.py is used as a utility library.
1104 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105
1106 if issue:
1107 assert codereview, 'codereview must be known, if issue is known'
1108
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.branchref = branchref
1110 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001111 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 self.branch = ShortBranchName(self.branchref)
1113 else:
1114 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001116 self.lookedup_issue = False
1117 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 self.has_description = False
1119 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001120 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001122 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001123 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001124 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001125 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001126
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001127 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001128 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001129 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001130 assert self._codereview_impl
1131 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001132
1133 def _load_codereview_impl(self, codereview=None, **kwargs):
1134 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001135 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1136 'codereview {} not in {}'.format(codereview,
1137 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001138 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1139 self._codereview = codereview
1140 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001141 return
1142
1143 # Automatic selection based on issue number set for a current branch.
1144 # Rietveld takes precedence over Gerrit.
1145 assert not self.issue
1146 # Whether we find issue or not, we are doing the lookup.
1147 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001148 if self.GetBranch():
1149 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1150 issue = _git_get_branch_config_value(
1151 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1152 if issue:
1153 self._codereview = codereview
1154 self._codereview_impl = cls(self, **kwargs)
1155 self.issue = int(issue)
1156 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157
Bryce Thomascfc97122018-12-13 20:21:47 +00001158 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001160 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161 **kwargs)
1162
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001163 def IsGerrit(self):
1164 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001165
1166 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001167 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001168
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001169 The return value is a string suitable for passing to git cl with the --cc
1170 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001171 """
1172 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001173 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001174 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001175 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1176 return self.cc
1177
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001178 def GetCCListWithoutDefault(self):
1179 """Return the users cc'd on this CL excluding default ones."""
1180 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001181 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001182 return self.cc
1183
Daniel Cheng7227d212017-11-17 08:12:37 -08001184 def ExtendCC(self, more_cc):
1185 """Extends the list of users to cc on this CL based on the changed files."""
1186 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188 def GetBranch(self):
1189 """Returns the short branch name, e.g. 'master'."""
1190 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001191 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001192 if not branchref:
1193 return None
1194 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 self.branch = ShortBranchName(self.branchref)
1196 return self.branch
1197
1198 def GetBranchRef(self):
1199 """Returns the full branch name, e.g. 'refs/heads/master'."""
1200 self.GetBranch() # Poke the lazy loader.
1201 return self.branchref
1202
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001203 def ClearBranch(self):
1204 """Clears cached branch data of this object."""
1205 self.branch = self.branchref = None
1206
tandrii5d48c322016-08-18 16:19:37 -07001207 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1208 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1209 kwargs['branch'] = self.GetBranch()
1210 return _git_get_branch_config_value(key, default, **kwargs)
1211
1212 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1213 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1214 assert self.GetBranch(), (
1215 'this CL must have an associated branch to %sset %s%s' %
1216 ('un' if value is None else '',
1217 key,
1218 '' if value is None else ' to %r' % value))
1219 kwargs['branch'] = self.GetBranch()
1220 return _git_set_branch_config_value(key, value, **kwargs)
1221
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001222 @staticmethod
1223 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001224 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 e.g. 'origin', 'refs/heads/master'
1226 """
1227 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001228 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1229
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001231 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001233 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1234 error_ok=True).strip()
1235 if upstream_branch:
1236 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001238 # Else, try to guess the origin remote.
1239 remote_branches = RunGit(['branch', '-r']).split()
1240 if 'origin/master' in remote_branches:
1241 # Fall back on origin/master if it exits.
1242 remote = 'origin'
1243 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001245 DieWithError(
1246 'Unable to determine default branch to diff against.\n'
1247 'Either pass complete "git diff"-style arguments, like\n'
1248 ' git cl upload origin/master\n'
1249 'or verify this branch is set up to track another \n'
1250 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251
1252 return remote, upstream_branch
1253
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001254 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001255 upstream_branch = self.GetUpstreamBranch()
1256 if not BranchExists(upstream_branch):
1257 DieWithError('The upstream for the current branch (%s) does not exist '
1258 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001259 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001260 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001261
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 def GetUpstreamBranch(self):
1263 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001265 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001266 upstream_branch = upstream_branch.replace('refs/heads/',
1267 'refs/remotes/%s/' % remote)
1268 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1269 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 self.upstream_branch = upstream_branch
1271 return self.upstream_branch
1272
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001274 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 remote, branch = None, self.GetBranch()
1276 seen_branches = set()
1277 while branch not in seen_branches:
1278 seen_branches.add(branch)
1279 remote, branch = self.FetchUpstreamTuple(branch)
1280 branch = ShortBranchName(branch)
1281 if remote != '.' or branch.startswith('refs/remotes'):
1282 break
1283 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001284 remotes = RunGit(['remote'], error_ok=True).split()
1285 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001287 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001289 logging.warn('Could not determine which remote this change is '
1290 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001291 else:
1292 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001293 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001294 branch = 'HEAD'
1295 if branch.startswith('refs/remotes'):
1296 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001297 elif branch.startswith('refs/branch-heads/'):
1298 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 else:
1300 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 return self._remote
1302
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 def GitSanityChecks(self, upstream_git_obj):
1304 """Checks git repo status and ensures diff is from local commits."""
1305
sbc@chromium.org79706062015-01-14 21:18:12 +00001306 if upstream_git_obj is None:
1307 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001308 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001309 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001310 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001311 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001312 return False
1313
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 # Verify the commit we're diffing against is in our current branch.
1315 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1316 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1317 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001318 print('ERROR: %s is not in the current branch. You may need to rebase '
1319 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 return False
1321
1322 # List the commits inside the diff, and verify they are all local.
1323 commits_in_diff = RunGit(
1324 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1325 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1326 remote_branch = remote_branch.strip()
1327 if code != 0:
1328 _, remote_branch = self.GetRemoteBranch()
1329
1330 commits_in_remote = RunGit(
1331 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1332
1333 common_commits = set(commits_in_diff) & set(commits_in_remote)
1334 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001335 print('ERROR: Your diff contains %d commits already in %s.\n'
1336 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1337 'the diff. If you are using a custom git flow, you can override'
1338 ' the reference used for this check with "git config '
1339 'gitcl.remotebranch <git-ref>".' % (
1340 len(common_commits), remote_branch, upstream_git_obj),
1341 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001342 return False
1343 return True
1344
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001345 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001346 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001347
1348 Returns None if it is not set.
1349 """
tandrii5d48c322016-08-18 16:19:37 -07001350 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001351
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352 def GetRemoteUrl(self):
1353 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1354
1355 Returns None if there is no remote.
1356 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001357 is_cached, value = self._cached_remote_url
1358 if is_cached:
1359 return value
1360
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001361 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001362 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1363
Edward Lemur298f2cf2019-02-22 21:40:39 +00001364 # Check if the remote url can be parsed as an URL.
1365 host = urlparse.urlparse(url).netloc
1366 if host:
1367 self._cached_remote_url = (True, url)
1368 return url
1369
1370 # If it cannot be parsed as an url, assume it is a local directory, probably
1371 # a git cache.
1372 logging.warning('"%s" doesn\'t appear to point to a git host. '
1373 'Interpreting it as a local directory.', url)
1374 if not os.path.isdir(url):
1375 logging.error(
1376 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1377 remote, url, self.GetBranch())
1378 return None
1379
1380 cache_path = url
1381 url = RunGit(['config', 'remote.%s.url' % remote],
1382 error_ok=True,
1383 cwd=url).strip()
1384
1385 host = urlparse.urlparse(url).netloc
1386 if not host:
1387 logging.error(
1388 'Remote "%(remote)s" for branch "%(branch)s" points to '
1389 '"%(cache_path)s", but it is misconfigured.\n'
1390 '"%(cache_path)s" must be a git repo and must have a remote named '
1391 '"%(remote)s" pointing to the git host.', {
1392 'remote': remote,
1393 'cache_path': cache_path,
1394 'branch': self.GetBranch()})
1395 return None
1396
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001397 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001398 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001400 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001401 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001402 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001403 self.issue = self._GitGetBranchConfigValue(
1404 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001405 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 return self.issue
1407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 def GetIssueURL(self):
1409 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 issue = self.GetIssue()
1411 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001412 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001413 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001415 def GetDescription(self, pretty=False, force=False):
1416 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001418 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 self.has_description = True
1420 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001421 # Set width to 72 columns + 2 space indent.
1422 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001424 lines = self.description.splitlines()
1425 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 return self.description
1427
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001428 def GetDescriptionFooters(self):
1429 """Returns (non_footer_lines, footers) for the commit message.
1430
1431 Returns:
1432 non_footer_lines (list(str)) - Simple list of description lines without
1433 any footer. The lines do not contain newlines, nor does the list contain
1434 the empty line between the message and the footers.
1435 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1436 [("Change-Id", "Ideadbeef...."), ...]
1437 """
1438 raw_description = self.GetDescription()
1439 msg_lines, _, footers = git_footers.split_footers(raw_description)
1440 if footers:
1441 msg_lines = msg_lines[:len(msg_lines)-1]
1442 return msg_lines, footers
1443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001445 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001446 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001447 self.patchset = self._GitGetBranchConfigValue(
1448 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 return self.patchset
1451
1452 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001453 """Set this branch's patchset. If patchset=0, clears the patchset."""
1454 assert self.GetBranch()
1455 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001456 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001457 else:
1458 self.patchset = int(patchset)
1459 self._GitSetBranchConfigValue(
1460 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001462 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001463 """Set this branch's issue. If issue isn't given, clears the issue."""
1464 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001466 issue = int(issue)
1467 self._GitSetBranchConfigValue(
1468 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001469 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001470 codereview_server = self._codereview_impl.GetCodereviewServer()
1471 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001472 self._GitSetBranchConfigValue(
1473 self._codereview_impl.CodereviewServerConfigKey(),
1474 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 else:
tandrii5d48c322016-08-18 16:19:37 -07001476 # Reset all of these just to be clean.
1477 reset_suffixes = [
1478 'last-upload-hash',
1479 self._codereview_impl.IssueConfigKey(),
1480 self._codereview_impl.PatchsetConfigKey(),
1481 self._codereview_impl.CodereviewServerConfigKey(),
1482 ] + self._PostUnsetIssueProperties()
1483 for prop in reset_suffixes:
1484 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001485 msg = RunGit(['log', '-1', '--format=%B']).strip()
1486 if msg and git_footers.get_footer_change_id(msg):
1487 print('WARNING: The change patched into this branch has a Change-Id. '
1488 'Removing it.')
1489 RunGit(['commit', '--amend', '-m',
1490 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001491 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001492 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493
dnjba1b0f32016-09-02 12:37:42 -07001494 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001495 if not self.GitSanityChecks(upstream_branch):
1496 DieWithError('\nGit sanity check failure')
1497
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001498 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001499 if not root:
1500 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001501 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001502
1503 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001504 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001505 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001506 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001507 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001508 except subprocess2.CalledProcessError:
1509 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001510 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001511 'This branch probably doesn\'t exist anymore. To reset the\n'
1512 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001513 ' git branch --set-upstream-to origin/master %s\n'
1514 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001515 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001516
maruel@chromium.org52424302012-08-29 15:14:30 +00001517 issue = self.GetIssue()
1518 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001519 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001520 description = self.GetDescription()
1521 else:
1522 # If the change was never uploaded, use the log messages of all commits
1523 # up to the branch point, as git cl upload will prefill the description
1524 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001525 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1526 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001527
1528 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001529 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001530 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001531 name,
1532 description,
1533 absroot,
1534 files,
1535 issue,
1536 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001537 author,
1538 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001539
dsansomee2d6fd92016-09-08 00:10:47 -07001540 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001541 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001542 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001543 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001544
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001545 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1546 """Sets the description for this CL remotely.
1547
1548 You can get description_lines and footers with GetDescriptionFooters.
1549
1550 Args:
1551 description_lines (list(str)) - List of CL description lines without
1552 newline characters.
1553 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1554 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1555 `List-Of-Tokens`). It will be case-normalized so that each token is
1556 title-cased.
1557 """
1558 new_description = '\n'.join(description_lines)
1559 if footers:
1560 new_description += '\n'
1561 for k, v in footers:
1562 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1563 if not git_footers.FOOTER_PATTERN.match(foot):
1564 raise ValueError('Invalid footer %r' % foot)
1565 new_description += foot + '\n'
1566 self.UpdateDescription(new_description, force)
1567
Edward Lesmes8e282792018-04-03 18:50:29 -04001568 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001569 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1570 try:
1571 return presubmit_support.DoPresubmitChecks(change, committing,
1572 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1573 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001574 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1575 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001576 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001577 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001578
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001579 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1580 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001581 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1582 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001583 else:
1584 # Assume url.
1585 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1586 urlparse.urlparse(issue_arg))
1587 if not parsed_issue_arg or not parsed_issue_arg.valid:
1588 DieWithError('Failed to parse issue argument "%s". '
1589 'Must be an issue number or a valid URL.' % issue_arg)
1590 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001591 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001592
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001593 def CMDUpload(self, options, git_diff_args, orig_args):
1594 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001595 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001596 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001597 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001598 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599 else:
1600 if self.GetBranch() is None:
1601 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1602
1603 # Default to diffing against common ancestor of upstream branch
1604 base_branch = self.GetCommonAncestorWithUpstream()
1605 git_diff_args = [base_branch, 'HEAD']
1606
Aaron Gablec4c40d12017-05-22 11:49:53 -07001607
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001608 # Fast best-effort checks to abort before running potentially
1609 # expensive hooks if uploading is likely to fail anyway. Passing these
1610 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001611 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001612 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613
1614 # Apply watchlists on upload.
1615 change = self.GetChange(base_branch, None)
1616 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1617 files = [f.LocalPath() for f in change.AffectedFiles()]
1618 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001619 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620
1621 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001622 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 # Set the reviewer list now so that presubmit checks can access it.
1624 change_description = ChangeDescription(change.FullDescriptionText())
1625 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001626 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001627 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001628 change)
1629 change.SetDescriptionText(change_description.description)
1630 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001631 may_prompt=not options.force,
1632 verbose=options.verbose,
1633 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001634 if not hook_results.should_continue():
1635 return 1
1636 if not options.reviewers and hook_results.reviewers:
1637 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001638 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639
Aaron Gable13101a62018-02-09 13:20:41 -08001640 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001641 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001643 _git_set_branch_config_value('last-upload-hash',
1644 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001645 # Run post upload hooks, if specified.
1646 if settings.GetRunPostUploadHook():
1647 presubmit_support.DoPostUploadExecuter(
1648 change,
1649 self,
1650 settings.GetRoot(),
1651 options.verbose,
1652 sys.stdout)
1653
1654 # Upload all dependencies if specified.
1655 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001656 print()
1657 print('--dependencies has been specified.')
1658 print('All dependent local branches will be re-uploaded.')
1659 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001660 # Remove the dependencies flag from args so that we do not end up in a
1661 # loop.
1662 orig_args.remove('--dependencies')
1663 ret = upload_branch_deps(self, orig_args)
1664 return ret
1665
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001666 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001667 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001668
1669 Issue must have been already uploaded and known.
1670 """
1671 assert new_state in _CQState.ALL_STATES
1672 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001673 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001674 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001675 return 0
1676 except KeyboardInterrupt:
1677 raise
1678 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001679 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001680 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001681 ' * Your project has no CQ,\n'
1682 ' * You don\'t have permission to change the CQ state,\n'
1683 ' * There\'s a bug in this code (see stack trace below).\n'
1684 'Consider specifying which bots to trigger manually or asking your '
1685 'project owners for permissions or contacting Chrome Infra at:\n'
1686 'https://www.chromium.org/infra\n\n' %
1687 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001688 # Still raise exception so that stack trace is printed.
1689 raise
1690
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001691 # Forward methods to codereview specific implementation.
1692
Aaron Gable636b13f2017-07-14 10:42:48 -07001693 def AddComment(self, message, publish=None):
1694 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001695
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001696 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001697 """Returns list of _CommentSummary for each comment.
1698
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001699 args:
1700 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001701 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001702 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001703
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001704 def CloseIssue(self):
1705 return self._codereview_impl.CloseIssue()
1706
1707 def GetStatus(self):
1708 return self._codereview_impl.GetStatus()
1709
1710 def GetCodereviewServer(self):
1711 return self._codereview_impl.GetCodereviewServer()
1712
tandriide281ae2016-10-12 06:02:30 -07001713 def GetIssueOwner(self):
1714 """Get owner from codereview, which may differ from this checkout."""
1715 return self._codereview_impl.GetIssueOwner()
1716
Edward Lemur707d70b2018-02-07 00:50:14 +01001717 def GetReviewers(self):
1718 return self._codereview_impl.GetReviewers()
1719
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001720 def GetMostRecentPatchset(self):
1721 return self._codereview_impl.GetMostRecentPatchset()
1722
tandriide281ae2016-10-12 06:02:30 -07001723 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001724 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001725 return self._codereview_impl.CannotTriggerTryJobReason()
1726
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001727 def GetTryJobProperties(self, patchset=None):
1728 """Returns dictionary of properties to launch try job."""
1729 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001730
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001731 def __getattr__(self, attr):
1732 # This is because lots of untested code accesses Rietveld-specific stuff
1733 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001734 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001735 # Note that child method defines __getattr__ as well, and forwards it here,
1736 # because _RietveldChangelistImpl is not cleaned up yet, and given
1737 # deprecation of Rietveld, it should probably be just removed.
1738 # Until that time, avoid infinite recursion by bypassing __getattr__
1739 # of implementation class.
1740 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741
1742
1743class _ChangelistCodereviewBase(object):
1744 """Abstract base class encapsulating codereview specifics of a changelist."""
1745 def __init__(self, changelist):
1746 self._changelist = changelist # instance of Changelist
1747
1748 def __getattr__(self, attr):
1749 # Forward methods to changelist.
1750 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1751 # _RietveldChangelistImpl to avoid this hack?
1752 return getattr(self._changelist, attr)
1753
1754 def GetStatus(self):
1755 """Apply a rough heuristic to give a simple summary of an issue's review
1756 or CQ status, assuming adherence to a common workflow.
1757
1758 Returns None if no issue for this branch, or specific string keywords.
1759 """
1760 raise NotImplementedError()
1761
1762 def GetCodereviewServer(self):
1763 """Returns server URL without end slash, like "https://codereview.com"."""
1764 raise NotImplementedError()
1765
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001766 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001767 """Fetches and returns description from the codereview server."""
1768 raise NotImplementedError()
1769
tandrii5d48c322016-08-18 16:19:37 -07001770 @classmethod
1771 def IssueConfigKey(cls):
1772 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001773 raise NotImplementedError()
1774
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001775 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001776 def PatchsetConfigKey(cls):
1777 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001778 raise NotImplementedError()
1779
tandrii5d48c322016-08-18 16:19:37 -07001780 @classmethod
1781 def CodereviewServerConfigKey(cls):
1782 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001783 raise NotImplementedError()
1784
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001785 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001786 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001787 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001788
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001789 def GetGerritObjForPresubmit(self):
1790 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1791 return None
1792
dsansomee2d6fd92016-09-08 00:10:47 -07001793 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 """Update the description on codereview site."""
1795 raise NotImplementedError()
1796
Aaron Gable636b13f2017-07-14 10:42:48 -07001797 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001798 """Posts a comment to the codereview site."""
1799 raise NotImplementedError()
1800
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001801 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001802 raise NotImplementedError()
1803
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 def CloseIssue(self):
1805 """Closes the issue."""
1806 raise NotImplementedError()
1807
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 def GetMostRecentPatchset(self):
1809 """Returns the most recent patchset number from the codereview site."""
1810 raise NotImplementedError()
1811
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001812 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001813 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001814 """Fetches and applies the issue.
1815
1816 Arguments:
1817 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1818 reject: if True, reject the failed patch instead of switching to 3-way
1819 merge. Rietveld only.
1820 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1821 only.
1822 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001823 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001824 """
1825 raise NotImplementedError()
1826
1827 @staticmethod
1828 def ParseIssueURL(parsed_url):
1829 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1830 failed."""
1831 raise NotImplementedError()
1832
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001833 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001834 """Best effort check that user is authenticated with codereview server.
1835
1836 Arguments:
1837 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001838 refresh: whether to attempt to refresh credentials. Ignored if not
1839 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001840 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001841 raise NotImplementedError()
1842
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001843 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001844 """Best effort check that uploading isn't supposed to fail for predictable
1845 reasons.
1846
1847 This method should raise informative exception if uploading shouldn't
1848 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001849
1850 Arguments:
1851 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001852 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001853 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001854
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001855 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001856 """Uploads a change to codereview."""
1857 raise NotImplementedError()
1858
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001859 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001860 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001861
1862 Issue must have been already uploaded and known.
1863 """
1864 raise NotImplementedError()
1865
tandriie113dfd2016-10-11 10:20:12 -07001866 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001867 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001868 raise NotImplementedError()
1869
tandriide281ae2016-10-12 06:02:30 -07001870 def GetIssueOwner(self):
1871 raise NotImplementedError()
1872
Edward Lemur707d70b2018-02-07 00:50:14 +01001873 def GetReviewers(self):
1874 raise NotImplementedError()
1875
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001876 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001877 raise NotImplementedError()
1878
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001879
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001880class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001881 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001882 # auth_config is Rietveld thing, kept here to preserve interface only.
1883 super(_GerritChangelistImpl, self).__init__(changelist)
1884 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001885 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001886 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001887 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001888 # Map from change number (issue) to its detail cache.
1889 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001891 if codereview_host is not None:
1892 assert not codereview_host.startswith('https://'), codereview_host
1893 self._gerrit_host = codereview_host
1894 self._gerrit_server = 'https://%s' % codereview_host
1895
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001896 def _GetGerritHost(self):
1897 # Lazy load of configs.
1898 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001899 if self._gerrit_host and '.' not in self._gerrit_host:
1900 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1901 # This happens for internal stuff http://crbug.com/614312.
1902 parsed = urlparse.urlparse(self.GetRemoteUrl())
1903 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001904 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001905 ' Your current remote is: %s' % self.GetRemoteUrl())
1906 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1907 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 return self._gerrit_host
1909
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001910 def _GetGitHost(self):
1911 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001912 remote_url = self.GetRemoteUrl()
1913 if not remote_url:
1914 return None
1915 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001916
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001917 def GetCodereviewServer(self):
1918 if not self._gerrit_server:
1919 # If we're on a branch then get the server potentially associated
1920 # with that branch.
1921 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001922 self._gerrit_server = self._GitGetBranchConfigValue(
1923 self.CodereviewServerConfigKey())
1924 if self._gerrit_server:
1925 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 if not self._gerrit_server:
1927 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1928 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001929 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 parts[0] = parts[0] + '-review'
1931 self._gerrit_host = '.'.join(parts)
1932 self._gerrit_server = 'https://%s' % self._gerrit_host
1933 return self._gerrit_server
1934
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001935 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001936 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001937 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001938 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001939 logging.warn('can\'t detect Gerrit project.')
1940 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001941 project = urlparse.urlparse(remote_url).path.strip('/')
1942 if project.endswith('.git'):
1943 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001944 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1945 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1946 # gitiles/git-over-https protocol. E.g.,
1947 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1948 # as
1949 # https://chromium.googlesource.com/v8/v8
1950 if project.startswith('a/'):
1951 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001952 return project
1953
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001954 def _GerritChangeIdentifier(self):
1955 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1956
1957 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001958 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001959 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001960 project = self._GetGerritProject()
1961 if project:
1962 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1963 # Fall back on still unique, but less efficient change number.
1964 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001965
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001966 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001967 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001968 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001969
tandrii5d48c322016-08-18 16:19:37 -07001970 @classmethod
1971 def PatchsetConfigKey(cls):
1972 return 'gerritpatchset'
1973
1974 @classmethod
1975 def CodereviewServerConfigKey(cls):
1976 return 'gerritserver'
1977
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001978 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001979 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001980 if settings.GetGerritSkipEnsureAuthenticated():
1981 # For projects with unusual authentication schemes.
1982 # See http://crbug.com/603378.
1983 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001984
1985 # Check presence of cookies only if using cookies-based auth method.
1986 cookie_auth = gerrit_util.Authenticator.get()
1987 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001988 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001989
1990 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001991 self.GetCodereviewServer()
1992 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001993 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001994
1995 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1996 git_auth = cookie_auth.get_auth_header(git_host)
1997 if gerrit_auth and git_auth:
1998 if gerrit_auth == git_auth:
1999 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002000 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002001 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002002 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002003 ' %s\n'
2004 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002005 ' Consider running the following command:\n'
2006 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002007 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002008 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002009 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002010 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002011 cookie_auth.get_new_password_message(git_host)))
2012 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002013 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002014 return
2015 else:
2016 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002017 ([] if gerrit_auth else [self._gerrit_host]) +
2018 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002019 DieWithError('Credentials for the following hosts are required:\n'
2020 ' %s\n'
2021 'These are read from %s (or legacy %s)\n'
2022 '%s' % (
2023 '\n '.join(missing),
2024 cookie_auth.get_gitcookies_path(),
2025 cookie_auth.get_netrc_path(),
2026 cookie_auth.get_new_password_message(git_host)))
2027
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002028 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002029 if not self.GetIssue():
2030 return
2031
2032 # Warm change details cache now to avoid RPCs later, reducing latency for
2033 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002034 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002035 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002036
2037 status = self._GetChangeDetail()['status']
2038 if status in ('MERGED', 'ABANDONED'):
2039 DieWithError('Change %s has been %s, new uploads are not allowed' %
2040 (self.GetIssueURL(),
2041 'submitted' if status == 'MERGED' else 'abandoned'))
2042
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002043 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2044 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2045 # Apparently this check is not very important? Otherwise get_auth_email
2046 # could have been added to other implementations of Authenticator.
2047 cookies_auth = gerrit_util.Authenticator.get()
2048 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002049 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002050
2051 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002052 if self.GetIssueOwner() == cookies_user:
2053 return
2054 logging.debug('change %s owner is %s, cookies user is %s',
2055 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002056 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002057 # so ask what Gerrit thinks of this user.
2058 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2059 if details['email'] == self.GetIssueOwner():
2060 return
2061 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002062 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002063 'as %s.\n'
2064 'Uploading may fail due to lack of permissions.' %
2065 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2066 confirm_or_exit(action='upload')
2067
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002068 def _PostUnsetIssueProperties(self):
2069 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002070 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002071
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002072 def GetGerritObjForPresubmit(self):
2073 return presubmit_support.GerritAccessor(self._GetGerritHost())
2074
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002075 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002076 """Apply a rough heuristic to give a simple summary of an issue's review
2077 or CQ status, assuming adherence to a common workflow.
2078
2079 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002080 * 'error' - error from review tool (including deleted issues)
2081 * 'unsent' - no reviewers added
2082 * 'waiting' - waiting for review
2083 * 'reply' - waiting for uploader to reply to review
2084 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002085 * 'dry-run' - dry-running in the commit queue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002086 * 'commit' - in the commit queue
2087 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002088 """
2089 if not self.GetIssue():
2090 return None
2091
2092 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002093 data = self._GetChangeDetail([
2094 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002095 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002096 return 'error'
2097
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002098 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002099 return 'closed'
2100
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002101 cq_label = data['labels'].get('Commit-Queue', {})
2102 max_cq_vote = 0
2103 for vote in cq_label.get('all', []):
2104 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2105 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002106 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002107 if max_cq_vote == 1:
2108 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002109
Aaron Gable9ab38c62017-04-06 14:36:33 -07002110 if data['labels'].get('Code-Review', {}).get('approved'):
2111 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002112
2113 if not data.get('reviewers', {}).get('REVIEWER', []):
2114 return 'unsent'
2115
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002116 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002117 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2118 last_message_author = messages.pop().get('author', {})
2119 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002120 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2121 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002122 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002123 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002124 if last_message_author.get('_account_id') == owner:
2125 # Most recent message was by owner.
2126 return 'waiting'
2127 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002128 # Some reply from non-owner.
2129 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002130
2131 # Somehow there are no messages even though there are reviewers.
2132 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002133
2134 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002135 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002136 patchset = data['revisions'][data['current_revision']]['_number']
2137 self.SetPatchset(patchset)
2138 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002139
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002140 def FetchDescription(self, force=False):
2141 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2142 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002143 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002144 return data['revisions'][current_rev]['commit']['message'].encode(
2145 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002146
dsansomee2d6fd92016-09-08 00:10:47 -07002147 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002148 if gerrit_util.HasPendingChangeEdit(
2149 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002150 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002151 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002152 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002153 'unpublished edit. Either publish the edit in the Gerrit web UI '
2154 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002155
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002156 gerrit_util.DeletePendingChangeEdit(
2157 self._GetGerritHost(), self._GerritChangeIdentifier())
2158 gerrit_util.SetCommitMessage(
2159 self._GetGerritHost(), self._GerritChangeIdentifier(),
2160 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002161
Aaron Gable636b13f2017-07-14 10:42:48 -07002162 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002163 gerrit_util.SetReview(
2164 self._GetGerritHost(), self._GerritChangeIdentifier(),
2165 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002166
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002167 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002168 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002169 # CURRENT_REVISION is included to get the latest patchset so that
2170 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002171 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002172 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2173 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002174 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002175 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002176 robot_file_comments = gerrit_util.GetChangeRobotComments(
2177 self._GetGerritHost(), self._GerritChangeIdentifier())
2178
2179 # Add the robot comments onto the list of comments, but only
2180 # keep those that are from the latest pachset.
2181 latest_patch_set = self.GetMostRecentPatchset()
2182 for path, robot_comments in robot_file_comments.iteritems():
2183 line_comments = file_comments.setdefault(path, [])
2184 line_comments.extend(
2185 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002186
2187 # Build dictionary of file comments for easy access and sorting later.
2188 # {author+date: {path: {patchset: {line: url+message}}}}
2189 comments = collections.defaultdict(
2190 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2191 for path, line_comments in file_comments.iteritems():
2192 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002193 tag = comment.get('tag', '')
2194 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002195 continue
2196 key = (comment['author']['email'], comment['updated'])
2197 if comment.get('side', 'REVISION') == 'PARENT':
2198 patchset = 'Base'
2199 else:
2200 patchset = 'PS%d' % comment['patch_set']
2201 line = comment.get('line', 0)
2202 url = ('https://%s/c/%s/%s/%s#%s%s' %
2203 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2204 'b' if comment.get('side') == 'PARENT' else '',
2205 str(line) if line else ''))
2206 comments[key][path][patchset][line] = (url, comment['message'])
2207
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002208 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002209 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002210 summary = self._BuildCommentSummary(msg, comments, readable)
2211 if summary:
2212 summaries.append(summary)
2213 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002214
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002215 @staticmethod
2216 def _BuildCommentSummary(msg, comments, readable):
2217 key = (msg['author']['email'], msg['date'])
2218 # Don't bother showing autogenerated messages that don't have associated
2219 # file or line comments. this will filter out most autogenerated
2220 # messages, but will keep robot comments like those from Tricium.
2221 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2222 if is_autogenerated and not comments.get(key):
2223 return None
2224 message = msg['message']
2225 # Gerrit spits out nanoseconds.
2226 assert len(msg['date'].split('.')[-1]) == 9
2227 date = datetime.datetime.strptime(msg['date'][:-3],
2228 '%Y-%m-%d %H:%M:%S.%f')
2229 if key in comments:
2230 message += '\n'
2231 for path, patchsets in sorted(comments.get(key, {}).items()):
2232 if readable:
2233 message += '\n%s' % path
2234 for patchset, lines in sorted(patchsets.items()):
2235 for line, (url, content) in sorted(lines.items()):
2236 if line:
2237 line_str = 'Line %d' % line
2238 path_str = '%s:%d:' % (path, line)
2239 else:
2240 line_str = 'File comment'
2241 path_str = '%s:0:' % path
2242 if readable:
2243 message += '\n %s, %s: %s' % (patchset, line_str, url)
2244 message += '\n %s\n' % content
2245 else:
2246 message += '\n%s ' % path_str
2247 message += '\n%s\n' % content
2248
2249 return _CommentSummary(
2250 date=date,
2251 message=message,
2252 sender=msg['author']['email'],
2253 autogenerated=is_autogenerated,
2254 # These could be inferred from the text messages and correlated with
2255 # Code-Review label maximum, however this is not reliable.
2256 # Leaving as is until the need arises.
2257 approval=False,
2258 disapproval=False,
2259 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002260
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002261 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002262 gerrit_util.AbandonChange(
2263 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002264
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002265 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002266 gerrit_util.SubmitChange(
2267 self._GetGerritHost(), self._GerritChangeIdentifier(),
2268 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002269
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002270 def _GetChangeDetail(self, options=None, no_cache=False):
2271 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002272
2273 If fresh data is needed, set no_cache=True which will clear cache and
2274 thus new data will be fetched from Gerrit.
2275 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002276 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002277 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002278
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002279 # Optimization to avoid multiple RPCs:
2280 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2281 'CURRENT_COMMIT' not in options):
2282 options.append('CURRENT_COMMIT')
2283
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002284 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002285 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002286 options = [o.upper() for o in options]
2287
2288 # Check in cache first unless no_cache is True.
2289 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002290 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002291 else:
2292 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002293 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002294 # Assumption: data fetched before with extra options is suitable
2295 # for return for a smaller set of options.
2296 # For example, if we cached data for
2297 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2298 # and request is for options=[CURRENT_REVISION],
2299 # THEN we can return prior cached data.
2300 if options_set.issubset(cached_options_set):
2301 return data
2302
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002303 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002304 data = gerrit_util.GetChangeDetail(
2305 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002306 except gerrit_util.GerritError as e:
2307 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002308 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002309 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002310
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002311 self._detail_cache.setdefault(cache_key, []).append(
2312 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002313 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002314
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002315 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002316 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002317 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002318 data = gerrit_util.GetChangeCommit(
2319 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002320 except gerrit_util.GerritError as e:
2321 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002322 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002323 raise
agable32978d92016-11-01 12:55:02 -07002324 return data
2325
Karen Qian40c19422019-03-13 21:28:29 +00002326 def _IsCqConfigured(self):
2327 detail = self._GetChangeDetail(['LABELS'])
2328 if not u'Commit-Queue' in detail.get('labels', {}):
2329 return False
2330 # TODO(crbug/753213): Remove temporary hack
2331 if ('https://chromium.googlesource.com/chromium/src' ==
2332 self._changelist.GetRemoteUrl() and
2333 detail['branch'].startswith('refs/branch-heads/')):
2334 return False
2335 return True
2336
Olivier Robin75ee7252018-04-13 10:02:56 +02002337 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002338 if git_common.is_dirty_git_tree('land'):
2339 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002340
tandriid60367b2016-06-22 05:25:12 -07002341 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002342 if not force and self._IsCqConfigured():
2343 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002344 'which can test and land changes for you. '
2345 'Are you sure you wish to bypass it?\n',
2346 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002347 differs = True
tandriic4344b52016-08-29 06:04:54 -07002348 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002349 # Note: git diff outputs nothing if there is no diff.
2350 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002351 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002352 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002353 if detail['current_revision'] == last_upload:
2354 differs = False
2355 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002356 print('WARNING: Local branch contents differ from latest uploaded '
2357 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002358 if differs:
2359 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002360 confirm_or_exit(
2361 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2362 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002363 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002364 elif not bypass_hooks:
2365 hook_results = self.RunHook(
2366 committing=True,
2367 may_prompt=not force,
2368 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002369 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2370 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002371 if not hook_results.should_continue():
2372 return 1
2373
2374 self.SubmitIssue(wait_for_merge=True)
2375 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002376 links = self._GetChangeCommit().get('web_links', [])
2377 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002378 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002379 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002380 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002381 return 0
2382
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002383 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002384 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002385 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002386 assert not directory
2387 assert parsed_issue_arg.valid
2388
2389 self._changelist.issue = parsed_issue_arg.issue
2390
2391 if parsed_issue_arg.hostname:
2392 self._gerrit_host = parsed_issue_arg.hostname
2393 self._gerrit_server = 'https://%s' % self._gerrit_host
2394
tandriic2405f52016-10-10 08:13:15 -07002395 try:
2396 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002397 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002398 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002399
2400 if not parsed_issue_arg.patchset:
2401 # Use current revision by default.
2402 revision_info = detail['revisions'][detail['current_revision']]
2403 patchset = int(revision_info['_number'])
2404 else:
2405 patchset = parsed_issue_arg.patchset
2406 for revision_info in detail['revisions'].itervalues():
2407 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2408 break
2409 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002410 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002411 (parsed_issue_arg.patchset, self.GetIssue()))
2412
Aaron Gable697a91b2018-01-19 15:20:15 -08002413 remote_url = self._changelist.GetRemoteUrl()
2414 if remote_url.endswith('.git'):
2415 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002416 remote_url = remote_url.rstrip('/')
2417
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002418 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002419 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002420
2421 if remote_url != fetch_info['url']:
2422 DieWithError('Trying to patch a change from %s but this repo appears '
2423 'to be %s.' % (fetch_info['url'], remote_url))
2424
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002425 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002426
Aaron Gable62619a32017-06-16 08:22:09 -07002427 if force:
2428 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2429 print('Checked out commit for change %i patchset %i locally' %
2430 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002431 elif nocommit:
2432 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2433 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002434 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002435 RunGit(['cherry-pick', 'FETCH_HEAD'])
2436 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002437 (parsed_issue_arg.issue, patchset))
2438 print('Note: this created a local commit which does not have '
2439 'the same hash as the one uploaded for review. This will make '
2440 'uploading changes based on top of this branch difficult.\n'
2441 'If you want to do that, use "git cl patch --force" instead.')
2442
Stefan Zagerd08043c2017-10-12 12:07:02 -07002443 if self.GetBranch():
2444 self.SetIssue(parsed_issue_arg.issue)
2445 self.SetPatchset(patchset)
2446 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2447 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2448 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2449 else:
2450 print('WARNING: You are in detached HEAD state.\n'
2451 'The patch has been applied to your checkout, but you will not be '
2452 'able to upload a new patch set to the gerrit issue.\n'
2453 'Try using the \'-b\' option if you would like to work on a '
2454 'branch and/or upload a new patch set.')
2455
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002456 return 0
2457
2458 @staticmethod
2459 def ParseIssueURL(parsed_url):
2460 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2461 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002462 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2463 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002464 # Short urls like https://domain/<issue_number> can be used, but don't allow
2465 # specifying the patchset (you'd 404), but we allow that here.
2466 if parsed_url.path == '/':
2467 part = parsed_url.fragment
2468 else:
2469 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002470 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002471 if match:
2472 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002473 issue=int(match.group(3)),
2474 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002475 hostname=parsed_url.netloc,
2476 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002477 return None
2478
tandrii16e0b4e2016-06-07 10:34:28 -07002479 def _GerritCommitMsgHookCheck(self, offer_removal):
2480 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2481 if not os.path.exists(hook):
2482 return
2483 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2484 # custom developer made one.
2485 data = gclient_utils.FileRead(hook)
2486 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2487 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002488 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002489 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002490 'and may interfere with it in subtle ways.\n'
2491 'We recommend you remove the commit-msg hook.')
2492 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002493 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002494 gclient_utils.rm_file_or_tree(hook)
2495 print('Gerrit commit-msg hook removed.')
2496 else:
2497 print('OK, will keep Gerrit commit-msg hook in place.')
2498
Edward Lemur0f58ae42019-04-30 17:24:12 +00002499 def _RunGitPushWithTraces(self, change_desc, refspec, refspec_opts):
2500 gclient_utils.safe_makedirs(TRACES_DIR)
2501
2502 # Create a temporary directory to store traces in. Traces will be compressed
2503 # and stored in a 'traces' dir inside depot_tools.
2504 traces_dir = tempfile.mkdtemp()
2505 trace_name = os.path.basename(traces_dir)
2506 traces_zip = os.path.join(TRACES_DIR, trace_name + '-traces')
2507 # Create a temporary dir to store git config and gitcookies in. It will be
2508 # compressed and stored next to the traces.
2509 git_info_dir = tempfile.mkdtemp()
2510 git_info_zip = os.path.join(TRACES_DIR, trace_name + '-git-info')
2511
2512 env = os.environ.copy()
2513 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2514 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2515 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2516 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2517 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2518
2519 try:
2520 push_returncode = 0
2521 before_push = time_time()
2522 push_stdout = gclient_utils.CheckCallAndFilter(
2523 ['git', 'push', self.GetRemoteUrl(), refspec],
2524 env=env,
2525 print_stdout=True,
2526 # Flush after every line: useful for seeing progress when running as
2527 # recipe.
2528 filter_fn=lambda _: sys.stdout.flush())
2529 except subprocess2.CalledProcessError as e:
2530 push_returncode = e.returncode
2531 DieWithError('Failed to create a change. Please examine output above '
2532 'for the reason of the failure.\n'
2533 'Hint: run command below to diagnose common Git/Gerrit '
2534 'credential problems:\n'
2535 ' git cl creds-check\n' +
2536 TRACES_MESSAGE % (traces_zip, git_info_zip),
2537 change_desc)
2538 finally:
2539 execution_time = time_time() - before_push
2540 metrics.collector.add_repeated('sub_commands', {
2541 'command': 'git push',
2542 'execution_time': execution_time,
2543 'exit_code': push_returncode,
2544 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2545 })
2546
2547 if push_returncode != 0:
2548 # Keep only the first 6 characters of the git hashes on the packet
2549 # trace. This greatly decreases size after compression.
2550 packet_traces = os.path.join(traces_dir, 'trace-packet')
2551 contents = gclient_utils.FileRead(packet_traces)
2552 gclient_utils.FileWrite(
2553 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2554 shutil.make_archive(traces_zip, 'zip', traces_dir)
2555
2556 # Collect and compress the git config and gitcookies.
2557 git_config = RunGit(['config', '-l'])
2558 gclient_utils.FileWrite(
2559 os.path.join(git_info_dir, 'git-config'),
2560 git_config)
2561
2562 cookie_auth = gerrit_util.Authenticator.get()
2563 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2564 gitcookies_path = cookie_auth.get_gitcookies_path()
2565 gitcookies = gclient_utils.FileRead(gitcookies_path)
2566 gclient_utils.FileWrite(
2567 os.path.join(git_info_dir, 'gitcookies'),
2568 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2569 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2570
2571 gclient_utils.rmtree(git_info_dir)
2572 gclient_utils.rmtree(traces_dir)
2573
2574 return push_stdout
2575
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002576 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002577 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002578 if options.squash and options.no_squash:
2579 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002580
2581 if not options.squash and not options.no_squash:
2582 # Load default for user, repo, squash=true, in this order.
2583 options.squash = settings.GetSquashGerritUploads()
2584 elif options.no_squash:
2585 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002586
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002587 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002588 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002589 # This may be None; default fallback value is determined in logic below.
2590 title = options.title
2591
Dominic Battre7d1c4842017-10-27 09:17:28 +02002592 # Extract bug number from branch name.
2593 bug = options.bug
2594 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2595 if not bug and match:
2596 bug = match.group(1)
2597
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002598 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002599 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002600 if self.GetIssue():
2601 # Try to get the message from a previous upload.
2602 message = self.GetDescription()
2603 if not message:
2604 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002605 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002606 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002607 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002608 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002609 # When uploading a subsequent patchset, -m|--message is taken
2610 # as the patchset title if --title was not provided.
2611 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002612 else:
2613 default_title = RunGit(
2614 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002615 if options.force:
2616 title = default_title
2617 else:
2618 title = ask_for_data(
2619 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002620 change_id = self._GetChangeDetail()['change_id']
2621 while True:
2622 footer_change_ids = git_footers.get_footer_change_id(message)
2623 if footer_change_ids == [change_id]:
2624 break
2625 if not footer_change_ids:
2626 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002627 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002628 continue
2629 # There is already a valid footer but with different or several ids.
2630 # Doing this automatically is non-trivial as we don't want to lose
2631 # existing other footers, yet we want to append just 1 desired
2632 # Change-Id. Thus, just create a new footer, but let user verify the
2633 # new description.
2634 message = '%s\n\nChange-Id: %s' % (message, change_id)
2635 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002636 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002637 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002638 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002639 'Please, check the proposed correction to the description, '
2640 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2641 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2642 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002643 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 if not options.force:
2645 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002646 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 message = change_desc.description
2648 if not message:
2649 DieWithError("Description is empty. Aborting...")
2650 # Continue the while loop.
2651 # Sanity check of this code - we should end up with proper message
2652 # footer.
2653 assert [change_id] == git_footers.get_footer_change_id(message)
2654 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002655 else: # if not self.GetIssue()
2656 if options.message:
2657 message = options.message
2658 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002659 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002660 if options.title:
2661 message = options.title + '\n\n' + message
2662 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002663
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002664 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002665 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002666 # On first upload, patchset title is always this string, while
2667 # --title flag gets converted to first line of message.
2668 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002669 if not change_desc.description:
2670 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002671 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002672 if len(change_ids) > 1:
2673 DieWithError('too many Change-Id footers, at most 1 allowed.')
2674 if not change_ids:
2675 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002676 change_desc.set_description(git_footers.add_footer_change_id(
2677 change_desc.description,
2678 GenerateGerritChangeId(change_desc.description)))
2679 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002680 assert len(change_ids) == 1
2681 change_id = change_ids[0]
2682
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002683 if options.reviewers or options.tbrs or options.add_owners_to:
2684 change_desc.update_reviewers(options.reviewers, options.tbrs,
2685 options.add_owners_to, change)
2686
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002687 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002688 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2689 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002690 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002691 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2692 desc_tempfile.write(change_desc.description)
2693 desc_tempfile.close()
2694 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2695 '-F', desc_tempfile.name]).strip()
2696 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 else:
2698 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002699 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 if not change_desc.description:
2701 DieWithError("Description is empty. Aborting...")
2702
2703 if not git_footers.get_footer_change_id(change_desc.description):
2704 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002705 change_desc.set_description(
2706 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002707 if options.reviewers or options.tbrs or options.add_owners_to:
2708 change_desc.update_reviewers(options.reviewers, options.tbrs,
2709 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002710 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002711 # For no-squash mode, we assume the remote called "origin" is the one we
2712 # want. It is not worthwhile to support different workflows for
2713 # no-squash mode.
2714 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2716
2717 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002718 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002719 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2720 ref_to_push)]).splitlines()
2721 if len(commits) > 1:
2722 print('WARNING: This will upload %d commits. Run the following command '
2723 'to see which commits will be uploaded: ' % len(commits))
2724 print('git log %s..%s' % (parent, ref_to_push))
2725 print('You can also use `git squash-branch` to squash these into a '
2726 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002727 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002729 if options.reviewers or options.tbrs or options.add_owners_to:
2730 change_desc.update_reviewers(options.reviewers, options.tbrs,
2731 options.add_owners_to, change)
2732
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002733 reviewers = sorted(change_desc.get_reviewers())
2734 # Add cc's from the CC_LIST and --cc flag (if any).
2735 if not options.private and not options.no_autocc:
2736 cc = self.GetCCList().split(',')
2737 else:
2738 cc = []
2739 if options.cc:
2740 cc.extend(options.cc)
2741 cc = filter(None, [email.strip() for email in cc])
2742 if change_desc.get_cced():
2743 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002744 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2745 valid_accounts = set(reviewers + cc)
2746 # TODO(crbug/877717): relax this for all hosts.
2747 else:
2748 valid_accounts = gerrit_util.ValidAccounts(
2749 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002750 logging.info('accounts %s are recognized, %s invalid',
2751 sorted(valid_accounts),
2752 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002753
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002754 # Extra options that can be specified at push time. Doc:
2755 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002756 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002757
Aaron Gable844cf292017-06-28 11:32:59 -07002758 # By default, new changes are started in WIP mode, and subsequent patchsets
2759 # don't send email. At any time, passing --send-mail will mark the change
2760 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002761 if options.send_mail:
2762 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002763 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002764 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002765 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002766 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002767 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002768
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002769 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002770 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002771
Aaron Gable9b713dd2016-12-14 16:04:21 -08002772 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002773 # Punctuation and whitespace in |title| must be percent-encoded.
2774 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002775
agablec6787972016-09-09 16:13:34 -07002776 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002777 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002778
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002779 for r in sorted(reviewers):
2780 if r in valid_accounts:
2781 refspec_opts.append('r=%s' % r)
2782 reviewers.remove(r)
2783 else:
2784 # TODO(tandrii): this should probably be a hard failure.
2785 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2786 % r)
2787 for c in sorted(cc):
2788 # refspec option will be rejected if cc doesn't correspond to an
2789 # account, even though REST call to add such arbitrary cc may succeed.
2790 if c in valid_accounts:
2791 refspec_opts.append('cc=%s' % c)
2792 cc.remove(c)
2793
rmistry9eadede2016-09-19 11:22:43 -07002794 if options.topic:
2795 # Documentation on Gerrit topics is here:
2796 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002797 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002798
Edward Lemur687ca902018-12-05 02:30:30 +00002799 if options.enable_auto_submit:
2800 refspec_opts.append('l=Auto-Submit+1')
2801 if options.use_commit_queue:
2802 refspec_opts.append('l=Commit-Queue+2')
2803 elif options.cq_dry_run:
2804 refspec_opts.append('l=Commit-Queue+1')
2805
2806 if change_desc.get_reviewers(tbr_only=True):
2807 score = gerrit_util.GetCodeReviewTbrScore(
2808 self._GetGerritHost(),
2809 self._GetGerritProject())
2810 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002811
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002812 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002813 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002814 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002815 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002816 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2817
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002818 refspec_suffix = ''
2819 if refspec_opts:
2820 refspec_suffix = '%' + ','.join(refspec_opts)
2821 assert ' ' not in refspec_suffix, (
2822 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2823 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2824
Edward Lemur0f58ae42019-04-30 17:24:12 +00002825 push_stdout = self._RunGitPushWithTraces(change_desc, refspec, refspec_opts)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002826
2827 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002828 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002829 change_numbers = [m.group(1)
2830 for m in map(regex.match, push_stdout.splitlines())
2831 if m]
2832 if len(change_numbers) != 1:
2833 DieWithError(
2834 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002835 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002836 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002837 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002838
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002839 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002840 # GetIssue() is not set in case of non-squash uploads according to tests.
2841 # TODO(agable): non-squash uploads in git cl should be removed.
2842 gerrit_util.AddReviewers(
2843 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002844 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002845 reviewers, cc,
2846 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002847
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002848 return 0
2849
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002850 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2851 change_desc):
2852 """Computes parent of the generated commit to be uploaded to Gerrit.
2853
2854 Returns revision or a ref name.
2855 """
2856 if custom_cl_base:
2857 # Try to avoid creating additional unintended CLs when uploading, unless
2858 # user wants to take this risk.
2859 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2860 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2861 local_ref_of_target_remote])
2862 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002863 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002864 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2865 'If you proceed with upload, more than 1 CL may be created by '
2866 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2867 'If you are certain that specified base `%s` has already been '
2868 'uploaded to Gerrit as another CL, you may proceed.\n' %
2869 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2870 if not force:
2871 confirm_or_exit(
2872 'Do you take responsibility for cleaning up potential mess '
2873 'resulting from proceeding with upload?',
2874 action='upload')
2875 return custom_cl_base
2876
Aaron Gablef97e33d2017-03-30 15:44:27 -07002877 if remote != '.':
2878 return self.GetCommonAncestorWithUpstream()
2879
2880 # If our upstream branch is local, we base our squashed commit on its
2881 # squashed version.
2882 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2883
Aaron Gablef97e33d2017-03-30 15:44:27 -07002884 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002885 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002886
2887 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002888 # TODO(tandrii): consider checking parent change in Gerrit and using its
2889 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2890 # the tree hash of the parent branch. The upside is less likely bogus
2891 # requests to reupload parent change just because it's uploadhash is
2892 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002893 parent = RunGit(['config',
2894 'branch.%s.gerritsquashhash' % upstream_branch_name],
2895 error_ok=True).strip()
2896 # Verify that the upstream branch has been uploaded too, otherwise
2897 # Gerrit will create additional CLs when uploading.
2898 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2899 RunGitSilent(['rev-parse', parent + ':'])):
2900 DieWithError(
2901 '\nUpload upstream branch %s first.\n'
2902 'It is likely that this branch has been rebased since its last '
2903 'upload, so you just need to upload it again.\n'
2904 '(If you uploaded it with --no-squash, then branch dependencies '
2905 'are not supported, and you should reupload with --squash.)'
2906 % upstream_branch_name,
2907 change_desc)
2908 return parent
2909
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002910 def _AddChangeIdToCommitMessage(self, options, args):
2911 """Re-commits using the current message, assumes the commit hook is in
2912 place.
2913 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002914 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002915 git_command = ['commit', '--amend', '-m', log_desc]
2916 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002917 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002918 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002919 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002920 return new_log_desc
2921 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002922 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002924 def SetCQState(self, new_state):
2925 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002926 vote_map = {
2927 _CQState.NONE: 0,
2928 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002929 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002930 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002931 labels = {'Commit-Queue': vote_map[new_state]}
2932 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002933 gerrit_util.SetReview(
2934 self._GetGerritHost(), self._GerritChangeIdentifier(),
2935 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002936
tandriie113dfd2016-10-11 10:20:12 -07002937 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002938 try:
2939 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002940 except GerritChangeNotExists:
2941 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002942
2943 if data['status'] in ('ABANDONED', 'MERGED'):
2944 return 'CL %s is closed' % self.GetIssue()
2945
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002946 def GetTryJobProperties(self, patchset=None):
2947 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002948 data = self._GetChangeDetail(['ALL_REVISIONS'])
2949 patchset = int(patchset or self.GetPatchset())
2950 assert patchset
2951 revision_data = None # Pylint wants it to be defined.
2952 for revision_data in data['revisions'].itervalues():
2953 if int(revision_data['_number']) == patchset:
2954 break
2955 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002956 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002957 (patchset, self.GetIssue()))
2958 return {
2959 'patch_issue': self.GetIssue(),
2960 'patch_set': patchset or self.GetPatchset(),
2961 'patch_project': data['project'],
2962 'patch_storage': 'gerrit',
2963 'patch_ref': revision_data['fetch']['http']['ref'],
2964 'patch_repository_url': revision_data['fetch']['http']['url'],
2965 'patch_gerrit_url': self.GetCodereviewServer(),
2966 }
tandriie113dfd2016-10-11 10:20:12 -07002967
tandriide281ae2016-10-12 06:02:30 -07002968 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002969 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002970
Edward Lemur707d70b2018-02-07 00:50:14 +01002971 def GetReviewers(self):
2972 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002973 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002974
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002975
2976_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002977 'gerrit': _GerritChangelistImpl,
2978}
2979
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002980
iannuccie53c9352016-08-17 14:40:40 -07002981def _add_codereview_issue_select_options(parser, extra=""):
2982 _add_codereview_select_options(parser)
2983
2984 text = ('Operate on this issue number instead of the current branch\'s '
2985 'implicit issue.')
2986 if extra:
2987 text += ' '+extra
2988 parser.add_option('-i', '--issue', type=int, help=text)
2989
2990
2991def _process_codereview_issue_select_options(parser, options):
2992 _process_codereview_select_options(parser, options)
2993 if options.issue is not None and not options.forced_codereview:
2994 parser.error('--issue must be specified with either --rietveld or --gerrit')
2995
2996
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002997def _add_codereview_select_options(parser):
2998 """Appends --gerrit and --rietveld options to force specific codereview."""
2999 parser.codereview_group = optparse.OptionGroup(
3000 parser, 'EXPERIMENTAL! Codereview override options')
3001 parser.add_option_group(parser.codereview_group)
3002 parser.codereview_group.add_option(
3003 '--gerrit', action='store_true',
3004 help='Force the use of Gerrit for codereview')
3005 parser.codereview_group.add_option(
3006 '--rietveld', action='store_true',
3007 help='Force the use of Rietveld for codereview')
3008
3009
3010def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003011 if options.rietveld:
3012 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003013 options.forced_codereview = None
3014 if options.gerrit:
3015 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003016
3017
tandriif9aefb72016-07-01 09:06:51 -07003018def _get_bug_line_values(default_project, bugs):
3019 """Given default_project and comma separated list of bugs, yields bug line
3020 values.
3021
3022 Each bug can be either:
3023 * a number, which is combined with default_project
3024 * string, which is left as is.
3025
3026 This function may produce more than one line, because bugdroid expects one
3027 project per line.
3028
3029 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3030 ['v8:123', 'chromium:789']
3031 """
3032 default_bugs = []
3033 others = []
3034 for bug in bugs.split(','):
3035 bug = bug.strip()
3036 if bug:
3037 try:
3038 default_bugs.append(int(bug))
3039 except ValueError:
3040 others.append(bug)
3041
3042 if default_bugs:
3043 default_bugs = ','.join(map(str, default_bugs))
3044 if default_project:
3045 yield '%s:%s' % (default_project, default_bugs)
3046 else:
3047 yield default_bugs
3048 for other in sorted(others):
3049 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3050 yield other
3051
3052
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003053class ChangeDescription(object):
3054 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003055 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003056 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003057 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003058 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003059 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3060 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3061 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3062 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003063
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003064 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003065 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003066
agable@chromium.org42c20792013-09-12 17:34:49 +00003067 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003068 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003069 return '\n'.join(self._description_lines)
3070
3071 def set_description(self, desc):
3072 if isinstance(desc, basestring):
3073 lines = desc.splitlines()
3074 else:
3075 lines = [line.rstrip() for line in desc]
3076 while lines and not lines[0]:
3077 lines.pop(0)
3078 while lines and not lines[-1]:
3079 lines.pop(-1)
3080 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003082 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3083 """Rewrites the R=/TBR= line(s) as a single line each.
3084
3085 Args:
3086 reviewers (list(str)) - list of additional emails to use for reviewers.
3087 tbrs (list(str)) - list of additional emails to use for TBRs.
3088 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3089 the change that are missing OWNER coverage. If this is not None, you
3090 must also pass a value for `change`.
3091 change (Change) - The Change that should be used for OWNERS lookups.
3092 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003094 assert isinstance(tbrs, list), tbrs
3095
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003096 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003097 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003098
3099 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003100 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003101
3102 reviewers = set(reviewers)
3103 tbrs = set(tbrs)
3104 LOOKUP = {
3105 'TBR': tbrs,
3106 'R': reviewers,
3107 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003108
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003109 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003110 regexp = re.compile(self.R_LINE)
3111 matches = [regexp.match(line) for line in self._description_lines]
3112 new_desc = [l for i, l in enumerate(self._description_lines)
3113 if not matches[i]]
3114 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003115
agable@chromium.org42c20792013-09-12 17:34:49 +00003116 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003117
3118 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003119 for match in matches:
3120 if not match:
3121 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003122 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3123
3124 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003125 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003126 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003127 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003128 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003129 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003130 LOOKUP[add_owners_to].update(
3131 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003132
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003133 # If any folks ended up in both groups, remove them from tbrs.
3134 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003135
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003136 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3137 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003138
3139 # Put the new lines in the description where the old first R= line was.
3140 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3141 if 0 <= line_loc < len(self._description_lines):
3142 if new_tbr_line:
3143 self._description_lines.insert(line_loc, new_tbr_line)
3144 if new_r_line:
3145 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003147 if new_r_line:
3148 self.append_footer(new_r_line)
3149 if new_tbr_line:
3150 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003151
Aaron Gable3a16ed12017-03-23 10:51:55 -07003152 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003153 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 self.set_description([
3155 '# Enter a description of the change.',
3156 '# This will be displayed on the codereview site.',
3157 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003158 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003159 '--------------------',
3160 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003161
agable@chromium.org42c20792013-09-12 17:34:49 +00003162 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003163 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003164 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003165 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003166 if git_footer:
3167 self.append_footer('Bug: %s' % ', '.join(values))
3168 else:
3169 for value in values:
3170 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003171
agable@chromium.org42c20792013-09-12 17:34:49 +00003172 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003173 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003174 if not content:
3175 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003176 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003177
Bruce Dawson2377b012018-01-11 16:46:49 -08003178 # Strip off comments and default inserted "Bug:" line.
3179 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003180 (line.startswith('#') or
3181 line.rstrip() == "Bug:" or
3182 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003183 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003184 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003185 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003186
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003187 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003188 """Adds a footer line to the description.
3189
3190 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3191 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3192 that Gerrit footers are always at the end.
3193 """
3194 parsed_footer_line = git_footers.parse_footer(line)
3195 if parsed_footer_line:
3196 # Line is a gerrit footer in the form: Footer-Key: any value.
3197 # Thus, must be appended observing Gerrit footer rules.
3198 self.set_description(
3199 git_footers.add_footer(self.description,
3200 key=parsed_footer_line[0],
3201 value=parsed_footer_line[1]))
3202 return
3203
3204 if not self._description_lines:
3205 self._description_lines.append(line)
3206 return
3207
3208 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3209 if gerrit_footers:
3210 # git_footers.split_footers ensures that there is an empty line before
3211 # actual (gerrit) footers, if any. We have to keep it that way.
3212 assert top_lines and top_lines[-1] == ''
3213 top_lines, separator = top_lines[:-1], top_lines[-1:]
3214 else:
3215 separator = [] # No need for separator if there are no gerrit_footers.
3216
3217 prev_line = top_lines[-1] if top_lines else ''
3218 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3219 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3220 top_lines.append('')
3221 top_lines.append(line)
3222 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003223
tandrii99a72f22016-08-17 14:33:24 -07003224 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003225 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003226 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003227 reviewers = [match.group(2).strip()
3228 for match in matches
3229 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003230 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003231
bradnelsond975b302016-10-23 12:20:23 -07003232 def get_cced(self):
3233 """Retrieves the list of reviewers."""
3234 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3235 cced = [match.group(2).strip() for match in matches if match]
3236 return cleanup_list(cced)
3237
Nodir Turakulov23b82142017-11-16 11:04:25 -08003238 def get_hash_tags(self):
3239 """Extracts and sanitizes a list of Gerrit hashtags."""
3240 subject = (self._description_lines or ('',))[0]
3241 subject = re.sub(
3242 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3243
3244 tags = []
3245 start = 0
3246 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3247 while True:
3248 m = bracket_exp.match(subject, start)
3249 if not m:
3250 break
3251 tags.append(self.sanitize_hash_tag(m.group(1)))
3252 start = m.end()
3253
3254 if not tags:
3255 # Try "Tag: " prefix.
3256 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3257 if m:
3258 tags.append(self.sanitize_hash_tag(m.group(1)))
3259 return tags
3260
3261 @classmethod
3262 def sanitize_hash_tag(cls, tag):
3263 """Returns a sanitized Gerrit hash tag.
3264
3265 A sanitized hashtag can be used as a git push refspec parameter value.
3266 """
3267 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3268
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003269 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3270 """Updates this commit description given the parent.
3271
3272 This is essentially what Gnumbd used to do.
3273 Consult https://goo.gl/WMmpDe for more details.
3274 """
3275 assert parent_msg # No, orphan branch creation isn't supported.
3276 assert parent_hash
3277 assert dest_ref
3278 parent_footer_map = git_footers.parse_footers(parent_msg)
3279 # This will also happily parse svn-position, which GnumbD is no longer
3280 # supporting. While we'd generate correct footers, the verifier plugin
3281 # installed in Gerrit will block such commit (ie git push below will fail).
3282 parent_position = git_footers.get_position(parent_footer_map)
3283
3284 # Cherry-picks may have last line obscuring their prior footers,
3285 # from git_footers perspective. This is also what Gnumbd did.
3286 cp_line = None
3287 if (self._description_lines and
3288 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3289 cp_line = self._description_lines.pop()
3290
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003291 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003292
3293 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3294 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003295 for i, line in enumerate(footer_lines):
3296 k, v = git_footers.parse_footer(line) or (None, None)
3297 if k and k.startswith('Cr-'):
3298 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003299
3300 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003301 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003302 if parent_position[0] == dest_ref:
3303 # Same branch as parent.
3304 number = int(parent_position[1]) + 1
3305 else:
3306 number = 1 # New branch, and extra lineage.
3307 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3308 int(parent_position[1])))
3309
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003310 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3311 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003312
3313 self._description_lines = top_lines
3314 if cp_line:
3315 self._description_lines.append(cp_line)
3316 if self._description_lines[-1] != '':
3317 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003318 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003319
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003320
Aaron Gablea1bab272017-04-11 16:38:18 -07003321def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003322 """Retrieves the reviewers that approved a CL from the issue properties with
3323 messages.
3324
3325 Note that the list may contain reviewers that are not committer, thus are not
3326 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003327
3328 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003329 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003330 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003331 return sorted(
3332 set(
3333 message['sender']
3334 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003335 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003336 )
3337 )
3338
3339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340def FindCodereviewSettingsFile(filename='codereview.settings'):
3341 """Finds the given file starting in the cwd and going up.
3342
3343 Only looks up to the top of the repository unless an
3344 'inherit-review-settings-ok' file exists in the root of the repository.
3345 """
3346 inherit_ok_file = 'inherit-review-settings-ok'
3347 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003348 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003349 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3350 root = '/'
3351 while True:
3352 if filename in os.listdir(cwd):
3353 if os.path.isfile(os.path.join(cwd, filename)):
3354 return open(os.path.join(cwd, filename))
3355 if cwd == root:
3356 break
3357 cwd = os.path.dirname(cwd)
3358
3359
3360def LoadCodereviewSettingsFromFile(fileobj):
3361 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003362 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003363
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003364 def SetProperty(name, setting, unset_error_ok=False):
3365 fullname = 'rietveld.' + name
3366 if setting in keyvals:
3367 RunGit(['config', fullname, keyvals[setting]])
3368 else:
3369 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3370
tandrii48df5812016-10-17 03:55:37 -07003371 if not keyvals.get('GERRIT_HOST', False):
3372 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003373 # Only server setting is required. Other settings can be absent.
3374 # In that case, we ignore errors raised during option deletion attempt.
3375 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3376 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3377 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003378 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003379 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3380 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003381 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3382 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003383
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003384 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003385 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003386
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003387 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003388 RunGit(['config', 'gerrit.squash-uploads',
3389 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003390
tandrii@chromium.org28253532016-04-14 13:46:56 +00003391 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003392 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003393 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003396 # should be of the form
3397 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3398 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003399 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3400 keyvals['ORIGIN_URL_CONFIG']])
3401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003402
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003403def urlretrieve(source, destination):
3404 """urllib is broken for SSL connections via a proxy therefore we
3405 can't use urllib.urlretrieve()."""
3406 with open(destination, 'w') as f:
3407 f.write(urllib2.urlopen(source).read())
3408
3409
ukai@chromium.org712d6102013-11-27 00:52:58 +00003410def hasSheBang(fname):
3411 """Checks fname is a #! script."""
3412 with open(fname) as f:
3413 return f.read(2).startswith('#!')
3414
3415
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003416# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3417def DownloadHooks(*args, **kwargs):
3418 pass
3419
3420
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003421def DownloadGerritHook(force):
3422 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003423
3424 Args:
3425 force: True to update hooks. False to install hooks if not present.
3426 """
3427 if not settings.GetIsGerrit():
3428 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003429 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003430 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3431 if not os.access(dst, os.X_OK):
3432 if os.path.exists(dst):
3433 if not force:
3434 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003435 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003436 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003437 if not hasSheBang(dst):
3438 DieWithError('Not a script: %s\n'
3439 'You need to download from\n%s\n'
3440 'into .git/hooks/commit-msg and '
3441 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003442 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3443 except Exception:
3444 if os.path.exists(dst):
3445 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003446 DieWithError('\nFailed to download hooks.\n'
3447 'You need to download from\n%s\n'
3448 'into .git/hooks/commit-msg and '
3449 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003450
3451
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003452class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003453 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003454
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003455 _GOOGLESOURCE = 'googlesource.com'
3456
3457 def __init__(self):
3458 # Cached list of [host, identity, source], where source is either
3459 # .gitcookies or .netrc.
3460 self._all_hosts = None
3461
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003462 def ensure_configured_gitcookies(self):
3463 """Runs checks and suggests fixes to make git use .gitcookies from default
3464 path."""
3465 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3466 configured_path = RunGitSilent(
3467 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003468 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003469 if configured_path:
3470 self._ensure_default_gitcookies_path(configured_path, default)
3471 else:
3472 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003473
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003474 @staticmethod
3475 def _ensure_default_gitcookies_path(configured_path, default_path):
3476 assert configured_path
3477 if configured_path == default_path:
3478 print('git is already configured to use your .gitcookies from %s' %
3479 configured_path)
3480 return
3481
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003482 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003483 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3484 (configured_path, default_path))
3485
3486 if not os.path.exists(configured_path):
3487 print('However, your configured .gitcookies file is missing.')
3488 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3489 action='reconfigure')
3490 RunGit(['config', '--global', 'http.cookiefile', default_path])
3491 return
3492
3493 if os.path.exists(default_path):
3494 print('WARNING: default .gitcookies file already exists %s' %
3495 default_path)
3496 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3497 default_path)
3498
3499 confirm_or_exit('Move existing .gitcookies to default location?',
3500 action='move')
3501 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003502 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003503 print('Moved and reconfigured git to use .gitcookies from %s' %
3504 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003505
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003506 @staticmethod
3507 def _configure_gitcookies_path(default_path):
3508 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3509 if os.path.exists(netrc_path):
3510 print('You seem to be using outdated .netrc for git credentials: %s' %
3511 netrc_path)
3512 print('This tool will guide you through setting up recommended '
3513 '.gitcookies store for git credentials.\n'
3514 '\n'
3515 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3516 ' git config --global --unset http.cookiefile\n'
3517 ' mv %s %s.backup\n\n' % (default_path, default_path))
3518 confirm_or_exit(action='setup .gitcookies')
3519 RunGit(['config', '--global', 'http.cookiefile', default_path])
3520 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003521
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003522 def get_hosts_with_creds(self, include_netrc=False):
3523 if self._all_hosts is None:
3524 a = gerrit_util.CookiesAuthenticator()
3525 self._all_hosts = [
3526 (h, u, s)
3527 for h, u, s in itertools.chain(
3528 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3529 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3530 )
3531 if h.endswith(self._GOOGLESOURCE)
3532 ]
3533
3534 if include_netrc:
3535 return self._all_hosts
3536 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3537
3538 def print_current_creds(self, include_netrc=False):
3539 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3540 if not hosts:
3541 print('No Git/Gerrit credentials found')
3542 return
3543 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3544 header = [('Host', 'User', 'Which file'),
3545 ['=' * l for l in lengths]]
3546 for row in (header + hosts):
3547 print('\t'.join((('%%+%ds' % l) % s)
3548 for l, s in zip(lengths, row)))
3549
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003550 @staticmethod
3551 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003552 """Parses identity "git-<username>.domain" into <username> and domain."""
3553 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003554 # distinguishable from sub-domains. But we do know typical domains:
3555 if identity.endswith('.chromium.org'):
3556 domain = 'chromium.org'
3557 username = identity[:-len('.chromium.org')]
3558 else:
3559 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003560 if username.startswith('git-'):
3561 username = username[len('git-'):]
3562 return username, domain
3563
3564 def _get_usernames_of_domain(self, domain):
3565 """Returns list of usernames referenced by .gitcookies in a given domain."""
3566 identities_by_domain = {}
3567 for _, identity, _ in self.get_hosts_with_creds():
3568 username, domain = self._parse_identity(identity)
3569 identities_by_domain.setdefault(domain, []).append(username)
3570 return identities_by_domain.get(domain)
3571
3572 def _canonical_git_googlesource_host(self, host):
3573 """Normalizes Gerrit hosts (with '-review') to Git host."""
3574 assert host.endswith(self._GOOGLESOURCE)
3575 # Prefix doesn't include '.' at the end.
3576 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3577 if prefix.endswith('-review'):
3578 prefix = prefix[:-len('-review')]
3579 return prefix + '.' + self._GOOGLESOURCE
3580
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003581 def _canonical_gerrit_googlesource_host(self, host):
3582 git_host = self._canonical_git_googlesource_host(host)
3583 prefix = git_host.split('.', 1)[0]
3584 return prefix + '-review.' + self._GOOGLESOURCE
3585
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003586 def _get_counterpart_host(self, host):
3587 assert host.endswith(self._GOOGLESOURCE)
3588 git = self._canonical_git_googlesource_host(host)
3589 gerrit = self._canonical_gerrit_googlesource_host(git)
3590 return git if gerrit == host else gerrit
3591
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003592 def has_generic_host(self):
3593 """Returns whether generic .googlesource.com has been configured.
3594
3595 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3596 """
3597 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3598 if host == '.' + self._GOOGLESOURCE:
3599 return True
3600 return False
3601
3602 def _get_git_gerrit_identity_pairs(self):
3603 """Returns map from canonic host to pair of identities (Git, Gerrit).
3604
3605 One of identities might be None, meaning not configured.
3606 """
3607 host_to_identity_pairs = {}
3608 for host, identity, _ in self.get_hosts_with_creds():
3609 canonical = self._canonical_git_googlesource_host(host)
3610 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3611 idx = 0 if canonical == host else 1
3612 pair[idx] = identity
3613 return host_to_identity_pairs
3614
3615 def get_partially_configured_hosts(self):
3616 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003617 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3618 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3619 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003620
3621 def get_conflicting_hosts(self):
3622 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003623 host
3624 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003625 if None not in (i1, i2) and i1 != i2)
3626
3627 def get_duplicated_hosts(self):
3628 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3629 return set(host for host, count in counters.iteritems() if count > 1)
3630
3631 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3632 'chromium.googlesource.com': 'chromium.org',
3633 'chrome-internal.googlesource.com': 'google.com',
3634 }
3635
3636 def get_hosts_with_wrong_identities(self):
3637 """Finds hosts which **likely** reference wrong identities.
3638
3639 Note: skips hosts which have conflicting identities for Git and Gerrit.
3640 """
3641 hosts = set()
3642 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3643 pair = self._get_git_gerrit_identity_pairs().get(host)
3644 if pair and pair[0] == pair[1]:
3645 _, domain = self._parse_identity(pair[0])
3646 if domain != expected:
3647 hosts.add(host)
3648 return hosts
3649
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003650 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003651 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003652 hosts = sorted(hosts)
3653 assert hosts
3654 if extra_column_func is None:
3655 extras = [''] * len(hosts)
3656 else:
3657 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003658 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3659 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003660 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003661 lines.append(tmpl % he)
3662 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003663
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003664 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003665 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003666 yield ('.googlesource.com wildcard record detected',
3667 ['Chrome Infrastructure team recommends to list full host names '
3668 'explicitly.'],
3669 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003670
3671 dups = self.get_duplicated_hosts()
3672 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003673 yield ('The following hosts were defined twice',
3674 self._format_hosts(dups),
3675 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003676
3677 partial = self.get_partially_configured_hosts()
3678 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003679 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3680 'These hosts are missing',
3681 self._format_hosts(partial, lambda host: 'but %s defined' %
3682 self._get_counterpart_host(host)),
3683 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003684
3685 conflicting = self.get_conflicting_hosts()
3686 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003687 yield ('The following Git hosts have differing credentials from their '
3688 'Gerrit counterparts',
3689 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3690 tuple(self._get_git_gerrit_identity_pairs()[host])),
3691 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003692
3693 wrong = self.get_hosts_with_wrong_identities()
3694 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003695 yield ('These hosts likely use wrong identity',
3696 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3697 (self._get_git_gerrit_identity_pairs()[host][0],
3698 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3699 wrong)
3700
3701 def find_and_report_problems(self):
3702 """Returns True if there was at least one problem, else False."""
3703 found = False
3704 bad_hosts = set()
3705 for title, sublines, hosts in self._find_problems():
3706 if not found:
3707 found = True
3708 print('\n\n.gitcookies problem report:\n')
3709 bad_hosts.update(hosts or [])
3710 print(' %s%s' % (title , (':' if sublines else '')))
3711 if sublines:
3712 print()
3713 print(' %s' % '\n '.join(sublines))
3714 print()
3715
3716 if bad_hosts:
3717 assert found
3718 print(' You can manually remove corresponding lines in your %s file and '
3719 'visit the following URLs with correct account to generate '
3720 'correct credential lines:\n' %
3721 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3722 print(' %s' % '\n '.join(sorted(set(
3723 gerrit_util.CookiesAuthenticator().get_new_password_url(
3724 self._canonical_git_googlesource_host(host))
3725 for host in bad_hosts
3726 ))))
3727 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003728
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003729
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003730@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003731def CMDcreds_check(parser, args):
3732 """Checks credentials and suggests changes."""
3733 _, _ = parser.parse_args(args)
3734
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003735 # Code below checks .gitcookies. Abort if using something else.
3736 authn = gerrit_util.Authenticator.get()
3737 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3738 if isinstance(authn, gerrit_util.GceAuthenticator):
3739 DieWithError(
3740 'This command is not designed for GCE, are you on a bot?\n'
3741 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3742 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003743 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003744 'This command is not designed for bot environment. It checks '
3745 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003746
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003747 checker = _GitCookiesChecker()
3748 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003749
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003750 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003751 checker.print_current_creds(include_netrc=True)
3752
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003753 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003754 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003755 return 0
3756 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003757
3758
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003759@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003760def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003761 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003762 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3763 branch = ShortBranchName(branchref)
3764 _, args = parser.parse_args(args)
3765 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003766 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003767 return RunGit(['config', 'branch.%s.base-url' % branch],
3768 error_ok=False).strip()
3769 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003770 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003771 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3772 error_ok=False).strip()
3773
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003774def color_for_status(status):
3775 """Maps a Changelist status to color, for CMDstatus and other tools."""
3776 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003777 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003778 'waiting': Fore.BLUE,
3779 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003780 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003781 'lgtm': Fore.GREEN,
3782 'commit': Fore.MAGENTA,
3783 'closed': Fore.CYAN,
3784 'error': Fore.WHITE,
3785 }.get(status, Fore.WHITE)
3786
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003787
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003788def get_cl_statuses(changes, fine_grained, max_processes=None):
3789 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003790
3791 If fine_grained is true, this will fetch CL statuses from the server.
3792 Otherwise, simply indicate if there's a matching url for the given branches.
3793
3794 If max_processes is specified, it is used as the maximum number of processes
3795 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3796 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003797
3798 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003799 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003800 if not changes:
3801 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003802
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003803 if not fine_grained:
3804 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003805 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003806 for cl in changes:
3807 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003808 return
3809
3810 # First, sort out authentication issues.
3811 logging.debug('ensuring credentials exist')
3812 for cl in changes:
3813 cl.EnsureAuthenticated(force=False, refresh=True)
3814
3815 def fetch(cl):
3816 try:
3817 return (cl, cl.GetStatus())
3818 except:
3819 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003820 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003821 raise
3822
3823 threads_count = len(changes)
3824 if max_processes:
3825 threads_count = max(1, min(threads_count, max_processes))
3826 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3827
3828 pool = ThreadPool(threads_count)
3829 fetched_cls = set()
3830 try:
3831 it = pool.imap_unordered(fetch, changes).__iter__()
3832 while True:
3833 try:
3834 cl, status = it.next(timeout=5)
3835 except multiprocessing.TimeoutError:
3836 break
3837 fetched_cls.add(cl)
3838 yield cl, status
3839 finally:
3840 pool.close()
3841
3842 # Add any branches that failed to fetch.
3843 for cl in set(changes) - fetched_cls:
3844 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003845
rmistry@google.com2dd99862015-06-22 12:22:18 +00003846
3847def upload_branch_deps(cl, args):
3848 """Uploads CLs of local branches that are dependents of the current branch.
3849
3850 If the local branch dependency tree looks like:
3851 test1 -> test2.1 -> test3.1
3852 -> test3.2
3853 -> test2.2 -> test3.3
3854
3855 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3856 run on the dependent branches in this order:
3857 test2.1, test3.1, test3.2, test2.2, test3.3
3858
3859 Note: This function does not rebase your local dependent branches. Use it when
3860 you make a change to the parent branch that will not conflict with its
3861 dependent branches, and you would like their dependencies updated in
3862 Rietveld.
3863 """
3864 if git_common.is_dirty_git_tree('upload-branch-deps'):
3865 return 1
3866
3867 root_branch = cl.GetBranch()
3868 if root_branch is None:
3869 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3870 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003871 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003872 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3873 'patchset dependencies without an uploaded CL.')
3874
3875 branches = RunGit(['for-each-ref',
3876 '--format=%(refname:short) %(upstream:short)',
3877 'refs/heads'])
3878 if not branches:
3879 print('No local branches found.')
3880 return 0
3881
3882 # Create a dictionary of all local branches to the branches that are dependent
3883 # on it.
3884 tracked_to_dependents = collections.defaultdict(list)
3885 for b in branches.splitlines():
3886 tokens = b.split()
3887 if len(tokens) == 2:
3888 branch_name, tracked = tokens
3889 tracked_to_dependents[tracked].append(branch_name)
3890
vapiera7fbd5a2016-06-16 09:17:49 -07003891 print()
3892 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003893 dependents = []
3894 def traverse_dependents_preorder(branch, padding=''):
3895 dependents_to_process = tracked_to_dependents.get(branch, [])
3896 padding += ' '
3897 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003898 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003899 dependents.append(dependent)
3900 traverse_dependents_preorder(dependent, padding)
3901 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003902 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003903
3904 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003905 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003906 return 0
3907
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003908 confirm_or_exit('This command will checkout all dependent branches and run '
3909 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003910
rmistry@google.com2dd99862015-06-22 12:22:18 +00003911 # Record all dependents that failed to upload.
3912 failures = {}
3913 # Go through all dependents, checkout the branch and upload.
3914 try:
3915 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003916 print()
3917 print('--------------------------------------')
3918 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003919 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003921 try:
3922 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003923 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003924 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003925 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003926 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003927 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003928 finally:
3929 # Swap back to the original root branch.
3930 RunGit(['checkout', '-q', root_branch])
3931
vapiera7fbd5a2016-06-16 09:17:49 -07003932 print()
3933 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003934 for dependent_branch in dependents:
3935 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003936 print(' %s : %s' % (dependent_branch, upload_status))
3937 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003938
3939 return 0
3940
3941
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003942@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003943def CMDarchive(parser, args):
3944 """Archives and deletes branches associated with closed changelists."""
3945 parser.add_option(
3946 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003947 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003948 parser.add_option(
3949 '-f', '--force', action='store_true',
3950 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003951 parser.add_option(
3952 '-d', '--dry-run', action='store_true',
3953 help='Skip the branch tagging and removal steps.')
3954 parser.add_option(
3955 '-t', '--notags', action='store_true',
3956 help='Do not tag archived branches. '
3957 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003958
3959 auth.add_auth_options(parser)
3960 options, args = parser.parse_args(args)
3961 if args:
3962 parser.error('Unsupported args: %s' % ' '.join(args))
3963 auth_config = auth.extract_auth_config_from_options(options)
3964
3965 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3966 if not branches:
3967 return 0
3968
vapiera7fbd5a2016-06-16 09:17:49 -07003969 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003970 changes = [Changelist(branchref=b, auth_config=auth_config)
3971 for b in branches.splitlines()]
3972 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3973 statuses = get_cl_statuses(changes,
3974 fine_grained=True,
3975 max_processes=options.maxjobs)
3976 proposal = [(cl.GetBranch(),
3977 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3978 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003979 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003980 proposal.sort()
3981
3982 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003983 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003984 return 0
3985
3986 current_branch = GetCurrentBranch()
3987
vapiera7fbd5a2016-06-16 09:17:49 -07003988 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003989 if options.notags:
3990 for next_item in proposal:
3991 print(' ' + next_item[0])
3992 else:
3993 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3994 for next_item in proposal:
3995 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003996
kmarshall9249e012016-08-23 12:02:16 -07003997 # Quit now on precondition failure or if instructed by the user, either
3998 # via an interactive prompt or by command line flags.
3999 if options.dry_run:
4000 print('\nNo changes were made (dry run).\n')
4001 return 0
4002 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004003 print('You are currently on a branch \'%s\' which is associated with a '
4004 'closed codereview issue, so archive cannot proceed. Please '
4005 'checkout another branch and run this command again.' %
4006 current_branch)
4007 return 1
kmarshall9249e012016-08-23 12:02:16 -07004008 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004009 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4010 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004011 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004012 return 1
4013
4014 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004015 if not options.notags:
4016 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004017 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004018
vapiera7fbd5a2016-06-16 09:17:49 -07004019 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004020
4021 return 0
4022
4023
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004024@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004025def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004026 """Show status of changelists.
4027
4028 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004029 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004030 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004031 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004032 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004033 - Magenta in the commit queue
4034 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004035 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004036
4037 Also see 'git cl comments'.
4038 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004039 parser.add_option(
4040 '--no-branch-color',
4041 action='store_true',
4042 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004044 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004045 parser.add_option('-f', '--fast', action='store_true',
4046 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004047 parser.add_option(
4048 '-j', '--maxjobs', action='store', type=int,
4049 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004050
4051 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004052 _add_codereview_issue_select_options(
4053 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004054 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004055 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004056 if args:
4057 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004058 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059
iannuccie53c9352016-08-17 14:40:40 -07004060 if options.issue is not None and not options.field:
4061 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004064 cl = Changelist(auth_config=auth_config, issue=options.issue,
4065 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068 elif options.field == 'id':
4069 issueid = cl.GetIssue()
4070 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004073 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004075 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004076 elif options.field == 'status':
4077 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 elif options.field == 'url':
4079 url = cl.GetIssueURL()
4080 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004081 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004082 return 0
4083
4084 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4085 if not branches:
4086 print('No local branch found.')
4087 return 0
4088
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004089 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004090 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004091 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004092 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004093 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004094 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004095 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004096
Daniel McArdlea23bf592019-02-12 00:25:12 +00004097 current_branch = GetCurrentBranch()
4098
4099 def FormatBranchName(branch, colorize=False):
4100 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4101 an asterisk when it is the current branch."""
4102
4103 asterisk = ""
4104 color = Fore.RESET
4105 if branch == current_branch:
4106 asterisk = "* "
4107 color = Fore.GREEN
4108 branch_name = ShortBranchName(branch)
4109
4110 if colorize:
4111 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004112 return asterisk + branch_name
4113
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004114 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004115
4116 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004117 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4118 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004119 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004120 c, status = output.next()
4121 branch_statuses[c.GetBranch()] = status
4122 status = branch_statuses.pop(branch)
4123 url = cl.GetIssueURL()
4124 if url and (not status or status == 'error'):
4125 # The issue probably doesn't exist anymore.
4126 url += ' (broken)'
4127
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004128 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004129 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004130 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004131 color = ''
4132 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004133 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004134
Alan Cuttera3be9a52019-03-04 18:50:33 +00004135 branch_display = FormatBranchName(branch)
4136 padding = ' ' * (alignment - len(branch_display))
4137 if not options.no_branch_color:
4138 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004139
Alan Cuttera3be9a52019-03-04 18:50:33 +00004140 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4141 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004142
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004144 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004145 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004146 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004147 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004148 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004149 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004150 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004151 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004152 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004153 print('Issue description:')
4154 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004155 return 0
4156
4157
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004158def colorize_CMDstatus_doc():
4159 """To be called once in main() to add colors to git cl status help."""
4160 colors = [i for i in dir(Fore) if i[0].isupper()]
4161
4162 def colorize_line(line):
4163 for color in colors:
4164 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004165 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004166 indent = len(line) - len(line.lstrip(' ')) + 1
4167 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4168 return line
4169
4170 lines = CMDstatus.__doc__.splitlines()
4171 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4172
4173
phajdan.jre328cf92016-08-22 04:12:17 -07004174def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004175 if path == '-':
4176 json.dump(contents, sys.stdout)
4177 else:
4178 with open(path, 'w') as f:
4179 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004180
4181
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004182@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004183@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004184def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004185 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004186
4187 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004188 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004189 parser.add_option('-r', '--reverse', action='store_true',
4190 help='Lookup the branch(es) for the specified issues. If '
4191 'no issues are specified, all branches with mapped '
4192 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004193 parser.add_option('--json',
4194 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004195 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004196 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004197 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004198
dnj@chromium.org406c4402015-03-03 17:22:28 +00004199 if options.reverse:
4200 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004201 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004202 # Reverse issue lookup.
4203 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004204
4205 git_config = {}
4206 for config in RunGit(['config', '--get-regexp',
4207 r'branch\..*issue']).splitlines():
4208 name, _space, val = config.partition(' ')
4209 git_config[name] = val
4210
dnj@chromium.org406c4402015-03-03 17:22:28 +00004211 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004212 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4213 config_key = _git_branch_config_key(ShortBranchName(branch),
4214 cls.IssueConfigKey())
4215 issue = git_config.get(config_key)
4216 if issue:
4217 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004218 if not args:
4219 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004220 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004221 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004222 try:
4223 issue_num = int(issue)
4224 except ValueError:
4225 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004226 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004227 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004228 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004229 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004230 if options.json:
4231 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004232 return 0
4233
4234 if len(args) > 0:
4235 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4236 if not issue.valid:
4237 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4238 'or no argument to list it.\n'
4239 'Maybe you want to run git cl status?')
4240 cl = Changelist(codereview=issue.codereview)
4241 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004242 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004243 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004244 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4245 if options.json:
4246 write_json(options.json, {
4247 'issue': cl.GetIssue(),
4248 'issue_url': cl.GetIssueURL(),
4249 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 return 0
4251
4252
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004253@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004254def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004255 """Shows or posts review comments for any changelist."""
4256 parser.add_option('-a', '--add-comment', dest='comment',
4257 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004258 parser.add_option('-p', '--publish', action='store_true',
4259 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004260 parser.add_option('-i', '--issue', dest='issue',
4261 help='review issue id (defaults to current issue). '
4262 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004263 parser.add_option('-m', '--machine-readable', dest='readable',
4264 action='store_false', default=True,
4265 help='output comments in a format compatible with '
4266 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004267 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004268 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004269 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004270 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004271 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004272 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004273 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004274
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004275 issue = None
4276 if options.issue:
4277 try:
4278 issue = int(options.issue)
4279 except ValueError:
4280 DieWithError('A review issue id is expected to be a number')
4281
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004282 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4283
4284 if not cl.IsGerrit():
4285 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004286
4287 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004288 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004289 return 0
4290
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004291 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4292 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004293 for comment in summary:
4294 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004295 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004296 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004297 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004298 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004299 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004300 elif comment.autogenerated:
4301 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004302 else:
4303 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004304 print('\n%s%s %s%s\n%s' % (
4305 color,
4306 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4307 comment.sender,
4308 Fore.RESET,
4309 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4310
smut@google.comc85ac942015-09-15 16:34:43 +00004311 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004312 def pre_serialize(c):
4313 dct = c.__dict__.copy()
4314 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4315 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004316 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004317 return 0
4318
4319
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004320@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004321@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004322def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004323 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004324 parser.add_option('-d', '--display', action='store_true',
4325 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004326 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004327 help='New description to set for this issue (- for stdin, '
4328 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004329 parser.add_option('-f', '--force', action='store_true',
4330 help='Delete any unpublished Gerrit edits for this issue '
4331 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004332
4333 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004334 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004335 options, args = parser.parse_args(args)
4336 _process_codereview_select_options(parser, options)
4337
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004338 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004339 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004340 target_issue_arg = ParseIssueNumberArgument(args[0],
4341 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004342 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004343 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004344
martiniss6eda05f2016-06-30 10:18:35 -07004345 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004346 'auth_config': auth.extract_auth_config_from_options(options),
4347 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004348 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004349 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004350 if target_issue_arg:
4351 kwargs['issue'] = target_issue_arg.issue
4352 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004353 if target_issue_arg.codereview and not options.forced_codereview:
4354 detected_codereview_from_url = True
4355 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004356
4357 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004358 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004359 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004360 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004361
4362 if detected_codereview_from_url:
4363 logging.info('canonical issue/change URL: %s (type: %s)\n',
4364 cl.GetIssueURL(), target_issue_arg.codereview)
4365
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004366 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004367
smut@google.com34fb6b12015-07-13 20:03:26 +00004368 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004370 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004371
4372 if options.new_description:
4373 text = options.new_description
4374 if text == '-':
4375 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004376 elif text == '+':
4377 base_branch = cl.GetCommonAncestorWithUpstream()
4378 change = cl.GetChange(base_branch, None, local_description=True)
4379 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004380
4381 description.set_description(text)
4382 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004383 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004384
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004385 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004386 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004387 return 0
4388
4389
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004390@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004391def CMDlint(parser, args):
4392 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004393 parser.add_option('--filter', action='append', metavar='-x,+y',
4394 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004395 auth.add_auth_options(parser)
4396 options, args = parser.parse_args(args)
4397 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004398
4399 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004400 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004401 try:
4402 import cpplint
4403 import cpplint_chromium
4404 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004405 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004406 return 1
4407
4408 # Change the current working directory before calling lint so that it
4409 # shows the correct base.
4410 previous_cwd = os.getcwd()
4411 os.chdir(settings.GetRoot())
4412 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004413 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004414 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4415 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004416 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004417 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004418 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004419
4420 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004421 command = args + files
4422 if options.filter:
4423 command = ['--filter=' + ','.join(options.filter)] + command
4424 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004425
4426 white_regex = re.compile(settings.GetLintRegex())
4427 black_regex = re.compile(settings.GetLintIgnoreRegex())
4428 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4429 for filename in filenames:
4430 if white_regex.match(filename):
4431 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004432 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004433 else:
4434 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4435 extra_check_functions)
4436 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004437 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004438 finally:
4439 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004440 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004441 if cpplint._cpplint_state.error_count != 0:
4442 return 1
4443 return 0
4444
4445
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004446@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004447def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004448 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004449 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004450 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004451 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004452 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004453 parser.add_option('--all', action='store_true',
4454 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004455 parser.add_option('--parallel', action='store_true',
4456 help='Run all tests specified by input_api.RunTests in all '
4457 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004458 auth.add_auth_options(parser)
4459 options, args = parser.parse_args(args)
4460 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004461
sbc@chromium.org71437c02015-04-09 19:29:40 +00004462 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 return 1
4465
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004466 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467 if args:
4468 base_branch = args[0]
4469 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004470 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004471 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472
Aaron Gable8076c282017-11-29 14:39:41 -08004473 if options.all:
4474 base_change = cl.GetChange(base_branch, None)
4475 files = [('M', f) for f in base_change.AllFiles()]
4476 change = presubmit_support.GitChange(
4477 base_change.Name(),
4478 base_change.FullDescriptionText(),
4479 base_change.RepositoryRoot(),
4480 files,
4481 base_change.issue,
4482 base_change.patchset,
4483 base_change.author_email,
4484 base_change._upstream)
4485 else:
4486 change = cl.GetChange(base_branch, None)
4487
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004488 cl.RunHook(
4489 committing=not options.upload,
4490 may_prompt=False,
4491 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004492 change=change,
4493 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004494 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004495
4496
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004497def GenerateGerritChangeId(message):
4498 """Returns Ixxxxxx...xxx change id.
4499
4500 Works the same way as
4501 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4502 but can be called on demand on all platforms.
4503
4504 The basic idea is to generate git hash of a state of the tree, original commit
4505 message, author/committer info and timestamps.
4506 """
4507 lines = []
4508 tree_hash = RunGitSilent(['write-tree'])
4509 lines.append('tree %s' % tree_hash.strip())
4510 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4511 if code == 0:
4512 lines.append('parent %s' % parent.strip())
4513 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4514 lines.append('author %s' % author.strip())
4515 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4516 lines.append('committer %s' % committer.strip())
4517 lines.append('')
4518 # Note: Gerrit's commit-hook actually cleans message of some lines and
4519 # whitespace. This code is not doing this, but it clearly won't decrease
4520 # entropy.
4521 lines.append(message)
4522 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004523 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004524 return 'I%s' % change_hash.strip()
4525
4526
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004527def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004528 """Computes the remote branch ref to use for the CL.
4529
4530 Args:
4531 remote (str): The git remote for the CL.
4532 remote_branch (str): The git remote branch for the CL.
4533 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004534 """
4535 if not (remote and remote_branch):
4536 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004537
wittman@chromium.org455dc922015-01-26 20:15:50 +00004538 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004539 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004540 # refs, which are then translated into the remote full symbolic refs
4541 # below.
4542 if '/' not in target_branch:
4543 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4544 else:
4545 prefix_replacements = (
4546 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4547 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4548 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4549 )
4550 match = None
4551 for regex, replacement in prefix_replacements:
4552 match = re.search(regex, target_branch)
4553 if match:
4554 remote_branch = target_branch.replace(match.group(0), replacement)
4555 break
4556 if not match:
4557 # This is a branch path but not one we recognize; use as-is.
4558 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004559 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4560 # Handle the refs that need to land in different refs.
4561 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004562
wittman@chromium.org455dc922015-01-26 20:15:50 +00004563 # Create the true path to the remote branch.
4564 # Does the following translation:
4565 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4566 # * refs/remotes/origin/master -> refs/heads/master
4567 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4568 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4569 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4570 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4571 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4572 'refs/heads/')
4573 elif remote_branch.startswith('refs/remotes/branch-heads'):
4574 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004575
wittman@chromium.org455dc922015-01-26 20:15:50 +00004576 return remote_branch
4577
4578
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004579def cleanup_list(l):
4580 """Fixes a list so that comma separated items are put as individual items.
4581
4582 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4583 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4584 """
4585 items = sum((i.split(',') for i in l), [])
4586 stripped_items = (i.strip() for i in items)
4587 return sorted(filter(None, stripped_items))
4588
4589
Aaron Gable4db38df2017-11-03 14:59:07 -07004590@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004591@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004592def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004593 """Uploads the current changelist to codereview.
4594
4595 Can skip dependency patchset uploads for a branch by running:
4596 git config branch.branch_name.skip-deps-uploads True
4597 To unset run:
4598 git config --unset branch.branch_name.skip-deps-uploads
4599 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004600
4601 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4602 a bug number, this bug number is automatically populated in the CL
4603 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004604
4605 If subject contains text in square brackets or has "<text>: " prefix, such
4606 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4607 [git-cl] add support for hashtags
4608 Foo bar: implement foo
4609 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004610 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004611 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4612 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004613 parser.add_option('--bypass-watchlists', action='store_true',
4614 dest='bypass_watchlists',
4615 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004616 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004617 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004618 parser.add_option('--message', '-m', dest='message',
4619 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004620 parser.add_option('-b', '--bug',
4621 help='pre-populate the bug number(s) for this issue. '
4622 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004623 parser.add_option('--message-file', dest='message_file',
4624 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004625 parser.add_option('--title', '-t', dest='title',
4626 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004627 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004628 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004629 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004630 parser.add_option('--tbrs',
4631 action='append', default=[],
4632 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004633 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004634 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004635 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004636 parser.add_option('--hashtag', dest='hashtags',
4637 action='append', default=[],
4638 help=('Gerrit hashtag for new CL; '
4639 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004640 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004641 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004642 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004643 help='tell the commit queue to commit this patchset; '
4644 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004645 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004646 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004647 metavar='TARGET',
4648 help='Apply CL to remote ref TARGET. ' +
4649 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004650 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004651 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004652 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004653 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004654 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004655 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004656 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4657 const='TBR', help='add a set of OWNERS to TBR')
4658 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4659 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004660 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4661 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004662 help='Send the patchset to do a CQ dry run right after '
4663 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004664 parser.add_option('--dependencies', action='store_true',
4665 help='Uploads CLs of all the local branches that depend on '
4666 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004667 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4668 help='Sends your change to the CQ after an approval. Only '
4669 'works on repos that have the Auto-Submit label '
4670 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004671 parser.add_option('--parallel', action='store_true',
4672 help='Run all tests specified by input_api.RunTests in all '
4673 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004674
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004675 parser.add_option('--no-autocc', action='store_true',
4676 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004677 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004678 help='Set the review private. This implies --no-autocc.')
4679
rmistry@google.com2dd99862015-06-22 12:22:18 +00004680 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004681 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004682 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004683 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004684 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004685 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004686
sbc@chromium.org71437c02015-04-09 19:29:40 +00004687 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004688 return 1
4689
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004690 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004691 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004692 options.cc = cleanup_list(options.cc)
4693
tandriib80458a2016-06-23 12:20:07 -07004694 if options.message_file:
4695 if options.message:
4696 parser.error('only one of --message and --message-file allowed.')
4697 options.message = gclient_utils.FileRead(options.message_file)
4698 options.message_file = None
4699
tandrii4d0545a2016-07-06 03:56:49 -07004700 if options.cq_dry_run and options.use_commit_queue:
4701 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4702
Aaron Gableedbc4132017-09-11 13:22:28 -07004703 if options.use_commit_queue:
4704 options.send_mail = True
4705
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004706 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4707 settings.GetIsGerrit()
4708
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004709 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004710 if not cl.IsGerrit():
4711 # Error out with instructions for repos not yet configured for Gerrit.
4712 print('=====================================')
4713 print('NOTICE: Rietveld is no longer supported. '
4714 'You can upload changes to Gerrit with')
4715 print(' git cl upload --gerrit')
4716 print('or set Gerrit to be your default code review tool with')
4717 print(' git config gerrit.host true')
4718 print('=====================================')
4719 return 1
4720
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004721 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004722
4723
Francois Dorayd42c6812017-05-30 15:10:20 -04004724@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004725@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004726def CMDsplit(parser, args):
4727 """Splits a branch into smaller branches and uploads CLs.
4728
4729 Creates a branch and uploads a CL for each group of files modified in the
4730 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004731 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004732 the shared OWNERS file.
4733 """
4734 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004735 help="A text file containing a CL description in which "
4736 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004737 parser.add_option("-c", "--comment", dest="comment_file",
4738 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004739 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4740 default=False,
4741 help="List the files and reviewers for each CL that would "
4742 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004743 parser.add_option("--cq-dry-run", action='store_true',
4744 help="If set, will do a cq dry run for each uploaded CL. "
4745 "Please be careful when doing this; more than ~10 CLs "
4746 "has the potential to overload our build "
4747 "infrastructure. Try to upload these not during high "
4748 "load times (usually 11-3 Mountain View time). Email "
4749 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004750 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4751 default=True,
4752 help='Sends your change to the CQ after an approval. Only '
4753 'works on repos that have the Auto-Submit label '
4754 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004755 options, _ = parser.parse_args(args)
4756
4757 if not options.description_file:
4758 parser.error('No --description flag specified.')
4759
4760 def WrappedCMDupload(args):
4761 return CMDupload(OptionParser(), args)
4762
4763 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004764 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004765 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004766
4767
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004768@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004769@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004770def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004771 """DEPRECATED: Used to commit the current changelist via git-svn."""
4772 message = ('git-cl no longer supports committing to SVN repositories via '
4773 'git-svn. You probably want to use `git cl land` instead.')
4774 print(message)
4775 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776
4777
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004778# Two special branches used by git cl land.
4779MERGE_BRANCH = 'git-cl-commit'
4780CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4781
4782
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004783@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004784@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004785def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004786 """Commits the current changelist via git.
4787
4788 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4789 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004790 """
4791 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4792 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004793 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004794 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004795 parser.add_option('--parallel', action='store_true',
4796 help='Run all tests specified by input_api.RunTests in all '
4797 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004798 auth.add_auth_options(parser)
4799 (options, args) = parser.parse_args(args)
4800 auth_config = auth.extract_auth_config_from_options(options)
4801
4802 cl = Changelist(auth_config=auth_config)
4803
Robert Iannucci2e73d432018-03-14 01:10:47 -07004804 if not cl.IsGerrit():
4805 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004806
Robert Iannucci2e73d432018-03-14 01:10:47 -07004807 if not cl.GetIssue():
4808 DieWithError('You must upload the change first to Gerrit.\n'
4809 ' If you would rather have `git cl land` upload '
4810 'automatically for you, see http://crbug.com/642759')
4811 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004812 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813
4814
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004815@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004816@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004817def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004818 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004819 parser.add_option('-b', dest='newbranch',
4820 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004821 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004822 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004823 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004824 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004825 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004826 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004827 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004828 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004830 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004831
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004832
4833 group = optparse.OptionGroup(
4834 parser,
4835 'Options for continuing work on the current issue uploaded from a '
4836 'different clone (e.g. different machine). Must be used independently '
4837 'from the other options. No issue number should be specified, and the '
4838 'branch must have an issue number associated with it')
4839 group.add_option('--reapply', action='store_true', dest='reapply',
4840 help='Reset the branch and reapply the issue.\n'
4841 'CAUTION: This will undo any local changes in this '
4842 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004843
4844 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004845 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004846 parser.add_option_group(group)
4847
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004848 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004849 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004850 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004851 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004852 auth_config = auth.extract_auth_config_from_options(options)
4853
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004854 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004855 if options.newbranch:
4856 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004857 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004858 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004859
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004860 cl = Changelist(auth_config=auth_config,
4861 codereview=options.forced_codereview)
4862 if not cl.GetIssue():
4863 parser.error('current branch must have an associated issue')
4864
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004865 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004866 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004867 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004868
4869 RunGit(['reset', '--hard', upstream])
4870 if options.pull:
4871 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004872
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004873 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4874 options.directory)
4875
4876 if len(args) != 1 or not args[0]:
4877 parser.error('Must specify issue number or url')
4878
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004879 target_issue_arg = ParseIssueNumberArgument(args[0],
4880 options.forced_codereview)
4881 if not target_issue_arg.valid:
4882 parser.error('invalid codereview url or CL id')
4883
4884 cl_kwargs = {
4885 'auth_config': auth_config,
4886 'codereview_host': target_issue_arg.hostname,
4887 'codereview': options.forced_codereview,
4888 }
4889 detected_codereview_from_url = False
4890 if target_issue_arg.codereview and not options.forced_codereview:
4891 detected_codereview_from_url = True
4892 cl_kwargs['codereview'] = target_issue_arg.codereview
4893 cl_kwargs['issue'] = target_issue_arg.issue
4894
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004895 # We don't want uncommitted changes mixed up with the patch.
4896 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004897 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004898
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004899 if options.newbranch:
4900 if options.force:
4901 RunGit(['branch', '-D', options.newbranch],
4902 stderr=subprocess2.PIPE, error_ok=True)
4903 RunGit(['new-branch', options.newbranch])
4904
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004905 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004906
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004907 if cl.IsGerrit():
4908 if options.reject:
4909 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004910 if options.directory:
4911 parser.error('--directory is not supported with Gerrit codereview.')
4912
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004913 if detected_codereview_from_url:
4914 print('canonical issue/change URL: %s (type: %s)\n' %
4915 (cl.GetIssueURL(), target_issue_arg.codereview))
4916
4917 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004918 options.nocommit, options.directory,
4919 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004920
4921
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004922def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004923 """Fetches the tree status and returns either 'open', 'closed',
4924 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004925 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004926 if url:
4927 status = urllib2.urlopen(url).read().lower()
4928 if status.find('closed') != -1 or status == '0':
4929 return 'closed'
4930 elif status.find('open') != -1 or status == '1':
4931 return 'open'
4932 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004933 return 'unset'
4934
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004936def GetTreeStatusReason():
4937 """Fetches the tree status from a json url and returns the message
4938 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004939 url = settings.GetTreeStatusUrl()
4940 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004941 connection = urllib2.urlopen(json_url)
4942 status = json.loads(connection.read())
4943 connection.close()
4944 return status['message']
4945
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004946
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004947@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004948def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004949 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004950 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004951 status = GetTreeStatus()
4952 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004953 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004954 return 2
4955
vapiera7fbd5a2016-06-16 09:17:49 -07004956 print('The tree is %s' % status)
4957 print()
4958 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004959 if status != 'open':
4960 return 1
4961 return 0
4962
4963
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004964@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004965def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004966 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004967 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004968 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004969 '-b', '--bot', action='append',
4970 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4971 'times to specify multiple builders. ex: '
4972 '"-b win_rel -b win_layout". See '
4973 'the try server waterfall for the builders name and the tests '
4974 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004975 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004976 '-B', '--bucket', default='',
4977 help=('Buildbucket bucket to send the try requests.'))
4978 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004979 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004980 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004981 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004982 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004983 help='Revision to use for the try job; default: the revision will '
4984 'be determined by the try recipe that builder runs, which usually '
4985 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004986 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004987 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004988 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004989 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004990 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004991 '--category', default='git_cl_try', help='Specify custom build category.')
4992 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004993 '--project',
4994 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004995 'in recipe to determine to which repository or directory to '
4996 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004997 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004998 '-p', '--property', dest='properties', action='append', default=[],
4999 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005000 'key2=value2 etc. The value will be treated as '
5001 'json if decodable, or as string otherwise. '
5002 'NOTE: using this may make your try job not usable for CQ, '
5003 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005004 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005005 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5006 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005007 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005008 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005009 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005010 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005011 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005012 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005013
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005014 if options.master and options.master.startswith('luci.'):
5015 parser.error(
5016 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005017 # Make sure that all properties are prop=value pairs.
5018 bad_params = [x for x in options.properties if '=' not in x]
5019 if bad_params:
5020 parser.error('Got properties with missing "=": %s' % bad_params)
5021
maruel@chromium.org15192402012-09-06 12:38:29 +00005022 if args:
5023 parser.error('Unknown arguments: %s' % args)
5024
Koji Ishii31c14782018-01-08 17:17:33 +09005025 cl = Changelist(auth_config=auth_config, issue=options.issue,
5026 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005027 if not cl.GetIssue():
5028 parser.error('Need to upload first')
5029
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005030 if cl.IsGerrit():
5031 # HACK: warm up Gerrit change detail cache to save on RPCs.
5032 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5033
tandriie113dfd2016-10-11 10:20:12 -07005034 error_message = cl.CannotTriggerTryJobReason()
5035 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005036 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005037
borenet6c0efe62016-10-19 08:13:29 -07005038 if options.bucket and options.master:
5039 parser.error('Only one of --bucket and --master may be used.')
5040
qyearsley1fdfcb62016-10-24 13:22:03 -07005041 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005042
qyearsleydd49f942016-10-28 11:57:22 -07005043 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5044 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005045 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005046 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005047 print('git cl try with no bots now defaults to CQ dry run.')
5048 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5049 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005050
borenet6c0efe62016-10-19 08:13:29 -07005051 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005052 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005053 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005054 'of bot requires an initial job from a parent (usually a builder). '
5055 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005056 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005057 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005058
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005059 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005060 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005061 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005062 except BuildbucketResponseException as ex:
5063 print('ERROR: %s' % ex)
5064 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005065 return 0
5066
5067
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005068@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005069def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005070 """Prints info about try jobs associated with current CL."""
5071 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005072 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005073 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005074 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005075 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005076 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005077 '--color', action='store_true', default=setup_color.IS_TTY,
5078 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005079 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005080 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5081 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005082 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005083 '--json', help=('Path of JSON output file to write try job results to,'
5084 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005085 parser.add_option_group(group)
5086 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005087 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005088 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005089 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005090 if args:
5091 parser.error('Unrecognized args: %s' % ' '.join(args))
5092
5093 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005094 cl = Changelist(
5095 issue=options.issue, codereview=options.forced_codereview,
5096 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005097 if not cl.GetIssue():
5098 parser.error('Need to upload first')
5099
tandrii221ab252016-10-06 08:12:04 -07005100 patchset = options.patchset
5101 if not patchset:
5102 patchset = cl.GetMostRecentPatchset()
5103 if not patchset:
5104 parser.error('Codereview doesn\'t know about issue %s. '
5105 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005106 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005107 cl.GetIssue())
5108
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005109 try:
tandrii221ab252016-10-06 08:12:04 -07005110 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005111 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005112 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005113 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005114 if options.json:
5115 write_try_results_json(options.json, jobs)
5116 else:
5117 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005118 return 0
5119
5120
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005121@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005122@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005123def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005124 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005125 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005126 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005127 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005129 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005130 if args:
5131 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005132 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005133 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005134 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005135 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005136
5137 # Clear configured merge-base, if there is one.
5138 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005139 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005140 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005141 return 0
5142
5143
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005144@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005145def CMDweb(parser, args):
5146 """Opens the current CL in the web browser."""
5147 _, args = parser.parse_args(args)
5148 if args:
5149 parser.error('Unrecognized args: %s' % ' '.join(args))
5150
5151 issue_url = Changelist().GetIssueURL()
5152 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005153 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005154 return 1
5155
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005156 # Redirect I/O before invoking browser to hide its output. For example, this
5157 # allows to hide "Created new window in existing browser session." message
5158 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5159 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005160 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005161 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005162 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005163 os.open(os.devnull, os.O_RDWR)
5164 try:
5165 webbrowser.open(issue_url)
5166 finally:
5167 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005168 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005169 return 0
5170
5171
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005172@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005173def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005174 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005175 parser.add_option('-d', '--dry-run', action='store_true',
5176 help='trigger in dry run mode')
5177 parser.add_option('-c', '--clear', action='store_true',
5178 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005179 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005180 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005181 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005182 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005183 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005184 if args:
5185 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005186 if options.dry_run and options.clear:
5187 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5188
iannuccie53c9352016-08-17 14:40:40 -07005189 cl = Changelist(auth_config=auth_config, issue=options.issue,
5190 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005191 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005192 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005193 elif options.dry_run:
5194 state = _CQState.DRY_RUN
5195 else:
5196 state = _CQState.COMMIT
5197 if not cl.GetIssue():
5198 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005199 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005200 return 0
5201
5202
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005203@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005204def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005205 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005206 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005207 auth.add_auth_options(parser)
5208 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005209 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005210 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005211 if args:
5212 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005213 cl = Changelist(auth_config=auth_config, issue=options.issue,
5214 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005215 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005216 if not cl.GetIssue():
5217 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005218 cl.CloseIssue()
5219 return 0
5220
5221
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005222@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005223def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005224 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005225 parser.add_option(
5226 '--stat',
5227 action='store_true',
5228 dest='stat',
5229 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005230 auth.add_auth_options(parser)
5231 options, args = parser.parse_args(args)
5232 auth_config = auth.extract_auth_config_from_options(options)
5233 if args:
5234 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005235
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005236 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005237 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005238 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005239 if not issue:
5240 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005241
Aaron Gablea718c3e2017-08-28 17:47:28 -07005242 base = cl._GitGetBranchConfigValue('last-upload-hash')
5243 if not base:
5244 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5245 if not base:
5246 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5247 revision_info = detail['revisions'][detail['current_revision']]
5248 fetch_info = revision_info['fetch']['http']
5249 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5250 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005251
Aaron Gablea718c3e2017-08-28 17:47:28 -07005252 cmd = ['git', 'diff']
5253 if options.stat:
5254 cmd.append('--stat')
5255 cmd.append(base)
5256 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005257
5258 return 0
5259
5260
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005261@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005262def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005263 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005264 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005265 '--ignore-current',
5266 action='store_true',
5267 help='Ignore the CL\'s current reviewers and start from scratch.')
5268 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005269 '--ignore-self',
5270 action='store_true',
5271 help='Do not consider CL\'s author as an owners.')
5272 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005273 '--no-color',
5274 action='store_true',
5275 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005276 parser.add_option(
5277 '--batch',
5278 action='store_true',
5279 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005280 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005281 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005282 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005283
5284 author = RunGit(['config', 'user.email']).strip() or None
5285
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005286 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005287
5288 if args:
5289 if len(args) > 1:
5290 parser.error('Unknown args')
5291 base_branch = args[0]
5292 else:
5293 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005294 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005295
5296 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005297 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5298
5299 if options.batch:
5300 db = owners.Database(change.RepositoryRoot(), file, os.path)
5301 print('\n'.join(db.reviewers_for(affected_files, author)))
5302 return 0
5303
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005304 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005305 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005306 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005307 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005308 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005309 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005310 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005311 override_files=change.OriginalOwnersFiles(),
5312 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005313
5314
Aiden Bennerc08566e2018-10-03 17:52:42 +00005315def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005316 """Generates a diff command."""
5317 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005318 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5319
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005320 if allow_prefix:
5321 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5322 # case that diff.noprefix is set in the user's git config.
5323 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5324 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005325 diff_cmd += ['--no-prefix']
5326
5327 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005328
5329 if args:
5330 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005331 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005332 diff_cmd.append(arg)
5333 else:
5334 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005335
5336 return diff_cmd
5337
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005338
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005339def MatchingFileType(file_name, extensions):
5340 """Returns true if the file name ends with one of the given extensions."""
5341 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005342
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005343
enne@chromium.org555cfe42014-01-29 18:21:39 +00005344@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005345@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005346def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005347 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005348 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005349 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005350 parser.add_option('--full', action='store_true',
5351 help='Reformat the full content of all touched files')
5352 parser.add_option('--dry-run', action='store_true',
5353 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005354 parser.add_option(
5355 '--python',
5356 action='store_true',
5357 default=None,
5358 help='Enables python formatting on all python files.')
5359 parser.add_option(
5360 '--no-python',
5361 action='store_true',
5362 dest='python',
5363 help='Disables python formatting on all python files. '
5364 'Takes precedence over --python. '
5365 'If neither --python or --no-python are set, python '
5366 'files that have a .style.yapf file in an ancestor '
5367 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005368 parser.add_option('--js', action='store_true',
5369 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005370 parser.add_option('--diff', action='store_true',
5371 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005372 parser.add_option('--presubmit', action='store_true',
5373 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005374 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005375
Daniel Chengc55eecf2016-12-30 03:11:02 -08005376 # Normalize any remaining args against the current path, so paths relative to
5377 # the current directory are still resolved as expected.
5378 args = [os.path.join(os.getcwd(), arg) for arg in args]
5379
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005380 # git diff generates paths against the root of the repository. Change
5381 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005382 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005383 if rel_base_path:
5384 os.chdir(rel_base_path)
5385
digit@chromium.org29e47272013-05-17 17:01:46 +00005386 # Grab the merge-base commit, i.e. the upstream commit of the current
5387 # branch when it was created or the last time it was rebased. This is
5388 # to cover the case where the user may have called "git fetch origin",
5389 # moving the origin branch to a newer commit, but hasn't rebased yet.
5390 upstream_commit = None
5391 cl = Changelist()
5392 upstream_branch = cl.GetUpstreamBranch()
5393 if upstream_branch:
5394 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5395 upstream_commit = upstream_commit.strip()
5396
5397 if not upstream_commit:
5398 DieWithError('Could not find base commit for this branch. '
5399 'Are you in detached state?')
5400
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005401 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5402 diff_output = RunGit(changed_files_cmd)
5403 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005404 # Filter out files deleted by this CL
5405 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005406
Christopher Lamc5ba6922017-01-24 11:19:14 +11005407 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005408 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005409
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005410 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5411 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5412 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005413 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005414
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005415 top_dir = os.path.normpath(
5416 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5417
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005418 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5419 # formatted. This is used to block during the presubmit.
5420 return_value = 0
5421
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005422 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005423 # Locate the clang-format binary in the checkout
5424 try:
5425 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005426 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005427 DieWithError(e)
5428
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005429 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005430 cmd = [clang_format_tool]
5431 if not opts.dry_run and not opts.diff:
5432 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005433 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005434 if opts.diff:
5435 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005436 else:
5437 env = os.environ.copy()
5438 env['PATH'] = str(os.path.dirname(clang_format_tool))
5439 try:
5440 script = clang_format.FindClangFormatScriptInChromiumTree(
5441 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005442 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005443 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005444
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005445 cmd = [sys.executable, script, '-p0']
5446 if not opts.dry_run and not opts.diff:
5447 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005448
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005449 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5450 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005451
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005452 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5453 if opts.diff:
5454 sys.stdout.write(stdout)
5455 if opts.dry_run and len(stdout) > 0:
5456 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005457
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005458 # Similar code to above, but using yapf on .py files rather than clang-format
5459 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005460 py_explicitly_disabled = opts.python is not None and not opts.python
5461 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005462 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5463 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5464 if sys.platform.startswith('win'):
5465 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005466
Aiden Bennerc08566e2018-10-03 17:52:42 +00005467 # If we couldn't find a yapf file we'll default to the chromium style
5468 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005469 chromium_default_yapf_style = os.path.join(depot_tools_path,
5470 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005471 # Used for caching.
5472 yapf_configs = {}
5473 for f in python_diff_files:
5474 # Find the yapf style config for the current file, defaults to depot
5475 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005476 _FindYapfConfigFile(f, yapf_configs, top_dir)
5477
5478 # Turn on python formatting by default if a yapf config is specified.
5479 # This breaks in the case of this repo though since the specified
5480 # style file is also the global default.
5481 if opts.python is None:
5482 filtered_py_files = []
5483 for f in python_diff_files:
5484 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5485 filtered_py_files.append(f)
5486 else:
5487 filtered_py_files = python_diff_files
5488
5489 # Note: yapf still seems to fix indentation of the entire file
5490 # even if line ranges are specified.
5491 # See https://github.com/google/yapf/issues/499
5492 if not opts.full and filtered_py_files:
5493 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5494
5495 for f in filtered_py_files:
5496 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5497 if yapf_config is None:
5498 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005499
5500 cmd = [yapf_tool, '--style', yapf_config, f]
5501
5502 has_formattable_lines = False
5503 if not opts.full:
5504 # Only run yapf over changed line ranges.
5505 for diff_start, diff_len in py_line_diffs[f]:
5506 diff_end = diff_start + diff_len - 1
5507 # Yapf errors out if diff_end < diff_start but this
5508 # is a valid line range diff for a removal.
5509 if diff_end >= diff_start:
5510 has_formattable_lines = True
5511 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5512 # If all line diffs were removals we have nothing to format.
5513 if not has_formattable_lines:
5514 continue
5515
5516 if opts.diff or opts.dry_run:
5517 cmd += ['--diff']
5518 # Will return non-zero exit code if non-empty diff.
5519 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5520 if opts.diff:
5521 sys.stdout.write(stdout)
5522 elif len(stdout) > 0:
5523 return_value = 2
5524 else:
5525 cmd += ['-i']
5526 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005527
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005528 # Dart's formatter does not have the nice property of only operating on
5529 # modified chunks, so hard code full.
5530 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005531 try:
5532 command = [dart_format.FindDartFmtToolInChromiumTree()]
5533 if not opts.dry_run and not opts.diff:
5534 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005535 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005536
ppi@chromium.org6593d932016-03-03 15:41:15 +00005537 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005538 if opts.dry_run and stdout:
5539 return_value = 2
5540 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005541 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5542 'found in this checkout. Files in other languages are still '
5543 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005544
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005545 # Format GN build files. Always run on full build files for canonical form.
5546 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005547 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005548 if opts.dry_run or opts.diff:
5549 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005550 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005551 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5552 shell=sys.platform == 'win32',
5553 cwd=top_dir)
5554 if opts.dry_run and gn_ret == 2:
5555 return_value = 2 # Not formatted.
5556 elif opts.diff and gn_ret == 2:
5557 # TODO this should compute and print the actual diff.
5558 print("This change has GN build file diff for " + gn_diff_file)
5559 elif gn_ret != 0:
5560 # For non-dry run cases (and non-2 return values for dry-run), a
5561 # nonzero error code indicates a failure, probably because the file
5562 # doesn't parse.
5563 DieWithError("gn format failed on " + gn_diff_file +
5564 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005565
Ilya Shermane081cbe2017-08-15 17:51:04 -07005566 # Skip the metrics formatting from the global presubmit hook. These files have
5567 # a separate presubmit hook that issues an error if the files need formatting,
5568 # whereas the top-level presubmit script merely issues a warning. Formatting
5569 # these files is somewhat slow, so it's important not to duplicate the work.
5570 if not opts.presubmit:
5571 for xml_dir in GetDirtyMetricsDirs(diff_files):
5572 tool_dir = os.path.join(top_dir, xml_dir)
5573 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5574 if opts.dry_run or opts.diff:
5575 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005576 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005577 if opts.diff:
5578 sys.stdout.write(stdout)
5579 if opts.dry_run and stdout:
5580 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005581
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005582 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005583
Steven Holte2e664bf2017-04-21 13:10:47 -07005584def GetDirtyMetricsDirs(diff_files):
5585 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5586 metrics_xml_dirs = [
5587 os.path.join('tools', 'metrics', 'actions'),
5588 os.path.join('tools', 'metrics', 'histograms'),
5589 os.path.join('tools', 'metrics', 'rappor'),
5590 os.path.join('tools', 'metrics', 'ukm')]
5591 for xml_dir in metrics_xml_dirs:
5592 if any(file.startswith(xml_dir) for file in xml_diff_files):
5593 yield xml_dir
5594
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005595
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005596@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005597@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005598def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005599 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005600 _, args = parser.parse_args(args)
5601
5602 if len(args) != 1:
5603 parser.print_help()
5604 return 1
5605
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005606 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005607 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005608 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005609
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005610 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005611
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005612 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005613 output = RunGit(['config', '--local', '--get-regexp',
5614 r'branch\..*\.%s' % issueprefix],
5615 error_ok=True)
5616 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005617 if issue == target_issue:
5618 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005619
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005620 branches = []
5621 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005622 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005623 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005624 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005625 return 1
5626 if len(branches) == 1:
5627 RunGit(['checkout', branches[0]])
5628 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005629 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005630 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005631 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005632 which = raw_input('Choose by index: ')
5633 try:
5634 RunGit(['checkout', branches[int(which)]])
5635 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005636 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005637 return 1
5638
5639 return 0
5640
5641
maruel@chromium.org29404b52014-09-08 22:58:00 +00005642def CMDlol(parser, args):
5643 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005644 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005645 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5646 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5647 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005648 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005649 return 0
5650
5651
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005652class OptionParser(optparse.OptionParser):
5653 """Creates the option parse and add --verbose support."""
5654 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005655 optparse.OptionParser.__init__(
5656 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005657 self.add_option(
5658 '-v', '--verbose', action='count', default=0,
5659 help='Use 2 times for more debugging info')
5660
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005661 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005662 try:
5663 return self._parse_args(args)
5664 finally:
5665 # Regardless of success or failure of args parsing, we want to report
5666 # metrics, but only after logging has been initialized (if parsing
5667 # succeeded).
5668 global settings
5669 settings = Settings()
5670
5671 if not metrics.DISABLE_METRICS_COLLECTION:
5672 # GetViewVCUrl ultimately calls logging method.
5673 project_url = settings.GetViewVCUrl().strip('/+')
5674 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5675 metrics.collector.add('project_urls', [project_url])
5676
5677 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005678 # Create an optparse.Values object that will store only the actual passed
5679 # options, without the defaults.
5680 actual_options = optparse.Values()
5681 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5682 # Create an optparse.Values object with the default options.
5683 options = optparse.Values(self.get_default_values().__dict__)
5684 # Update it with the options passed by the user.
5685 options._update_careful(actual_options.__dict__)
5686 # Store the options passed by the user in an _actual_options attribute.
5687 # We store only the keys, and not the values, since the values can contain
5688 # arbitrary information, which might be PII.
5689 metrics.collector.add('arguments', actual_options.__dict__.keys())
5690
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005691 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005692 logging.basicConfig(
5693 level=levels[min(options.verbose, len(levels) - 1)],
5694 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5695 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005696
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005697 return options, args
5698
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005700def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005701 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005702 print('\nYour python version %s is unsupported, please upgrade.\n' %
5703 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005704 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005705
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005706 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005707 dispatcher = subcommand.CommandDispatcher(__name__)
5708 try:
5709 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005710 except auth.AuthenticationError as e:
5711 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005712 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005713 if e.code != 500:
5714 raise
5715 DieWithError(
5716 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5717 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005718 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005719
5720
5721if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005722 # These affect sys.stdout so do it outside of main() to simplify mocks in
5723 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005724 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005725 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005726 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005727 sys.exit(main(sys.argv[1:]))