blob: 2a0f47a9a31195cf48ccf8214f58acc1e8210244 [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
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000048import auth
skobes6468b902016-10-24 08:45:10 -070049import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000050import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000051import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000052import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000053import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000054import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000055import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000056import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000057import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000058import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000059import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000060import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000061import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000062import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000064import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040066import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000067import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000068import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069import watchlists
70
tandrii7400cf02016-06-21 08:48:07 -070071__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000072
tandrii9d2c7a32016-06-22 03:42:45 -070073COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070074DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080075POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000077REFS_THAT_ALIAS_TO_OTHER_REFS = {
78 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
79 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
80}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081
thestig@chromium.org44202a22014-03-11 19:22:18 +000082# Valid extensions for files we want to lint.
83DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
84DEFAULT_LINT_IGNORE_REGEX = r"$^"
85
Aiden Bennerc08566e2018-10-03 17:52:42 +000086# File name for yapf style config files.
87YAPF_CONFIG_FILENAME = '.style.yapf'
88
borenet6c0efe62016-10-19 08:13:29 -070089# Buildbucket master name prefix.
90MASTER_PREFIX = 'master.'
91
Edward Lemur83bd7f42018-10-10 00:14:21 +000092# TODO(crbug.com/881860): Remove
93# Log gerrit failures to a gerrit_util.GERRIT_ERR_LOG_FILE.
94GERRIT_ERR_LOGGER = logging.getLogger('GerritErrorLogs')
95
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000096# Shortcut since it quickly becomes redundant.
97Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000098
maruel@chromium.orgddd59412011-11-30 14:20:38 +000099# Initialized in main()
100settings = None
101
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100102# Used by tests/git_cl_test.py to add extra logging.
103# Inside the weirdly failing test, add this:
104# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700105# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100106_IS_BEING_TESTED = False
107
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000108
Christopher Lamf732cd52017-01-24 12:40:11 +1100109def DieWithError(message, change_desc=None):
110 if change_desc:
111 SaveDescriptionBackup(change_desc)
112
vapiera7fbd5a2016-06-16 09:17:49 -0700113 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114 sys.exit(1)
115
116
Christopher Lamf732cd52017-01-24 12:40:11 +1100117def SaveDescriptionBackup(change_desc):
118 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000119 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100120 backup_file = open(backup_path, 'w')
121 backup_file.write(change_desc.description)
122 backup_file.close()
123
124
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000125def GetNoGitPagerEnv():
126 env = os.environ.copy()
127 # 'cat' is a magical git string that disables pagers on all platforms.
128 env['GIT_PAGER'] = 'cat'
129 return env
130
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000131
bsep@chromium.org627d9002016-04-29 00:00:52 +0000132def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000134 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000135 except subprocess2.CalledProcessError as e:
136 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 'Command "%s" failed.\n%s' % (
140 ' '.join(args), error_message or e.stdout or ''))
141 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000142
143
144def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000145 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000146 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000147
148
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000149def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000150 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700151 if suppress_stderr:
152 stderr = subprocess2.VOID
153 else:
154 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000155 try:
tandrii5d48c322016-08-18 16:19:37 -0700156 (out, _), code = subprocess2.communicate(['git'] + args,
157 env=GetNoGitPagerEnv(),
158 stdout=subprocess2.PIPE,
159 stderr=stderr)
160 return code, out
161 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900162 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700163 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164
165
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000166def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000167 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000168 return RunGitWithCode(args, suppress_stderr=True)[1]
169
170
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000171def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000172 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000173 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000174 return (version.startswith(prefix) and
175 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000176
177
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000178def BranchExists(branch):
179 """Return True if specified branch exists."""
180 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
181 suppress_stderr=True)
182 return not code
183
184
tandrii2a16b952016-10-19 07:09:44 -0700185def time_sleep(seconds):
186 # Use this so that it can be mocked in tests without interfering with python
187 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700188 return time.sleep(seconds)
189
190
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000191def time_time():
192 # Use this so that it can be mocked in tests without interfering with python
193 # system machinery.
194 return time.time()
195
196
maruel@chromium.org90541732011-04-01 17:54:18 +0000197def ask_for_data(prompt):
198 try:
199 return raw_input(prompt)
200 except KeyboardInterrupt:
201 # Hide the exception.
202 sys.exit(1)
203
204
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100205def confirm_or_exit(prefix='', action='confirm'):
206 """Asks user to press enter to continue or press Ctrl+C to abort."""
207 if not prefix or prefix.endswith('\n'):
208 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100209 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100210 mid = ' Press'
211 elif prefix.endswith(' '):
212 mid = 'press'
213 else:
214 mid = ' press'
215 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
216
217
218def ask_for_explicit_yes(prompt):
219 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
220 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
221 while True:
222 if 'yes'.startswith(result):
223 return True
224 if 'no'.startswith(result):
225 return False
226 result = ask_for_data('Please, type yes or no: ').lower()
227
228
tandrii5d48c322016-08-18 16:19:37 -0700229def _git_branch_config_key(branch, key):
230 """Helper method to return Git config key for a branch."""
231 assert branch, 'branch name is required to set git config for it'
232 return 'branch.%s.%s' % (branch, key)
233
234
235def _git_get_branch_config_value(key, default=None, value_type=str,
236 branch=False):
237 """Returns git config value of given or current branch if any.
238
239 Returns default in all other cases.
240 """
241 assert value_type in (int, str, bool)
242 if branch is False: # Distinguishing default arg value from None.
243 branch = GetCurrentBranch()
244
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000245 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700246 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000247
tandrii5d48c322016-08-18 16:19:37 -0700248 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700249 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700250 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700251 # git config also has --int, but apparently git config suffers from integer
252 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700253 args.append(_git_branch_config_key(branch, key))
254 code, out = RunGitWithCode(args)
255 if code == 0:
256 value = out.strip()
257 if value_type == int:
258 return int(value)
259 if value_type == bool:
260 return bool(value.lower() == 'true')
261 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000262 return default
263
264
tandrii5d48c322016-08-18 16:19:37 -0700265def _git_set_branch_config_value(key, value, branch=None, **kwargs):
266 """Sets the value or unsets if it's None of a git branch config.
267
268 Valid, though not necessarily existing, branch must be provided,
269 otherwise currently checked out branch is used.
270 """
271 if not branch:
272 branch = GetCurrentBranch()
273 assert branch, 'a branch name OR currently checked out branch is required'
274 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700275 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700276 if value is None:
277 args.append('--unset')
278 elif isinstance(value, bool):
279 args.append('--bool')
280 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700281 else:
tandrii33a46ff2016-08-23 05:53:40 -0700282 # git config also has --int, but apparently git config suffers from integer
283 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700284 value = str(value)
285 args.append(_git_branch_config_key(branch, key))
286 if value is not None:
287 args.append(value)
288 RunGit(args, **kwargs)
289
290
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100291def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700292 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100293
294 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
295 """
296 # Git also stores timezone offset, but it only affects visual display,
297 # actual point in time is defined by this timestamp only.
298 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
299
300
301def _git_amend_head(message, committer_timestamp):
302 """Amends commit with new message and desired committer_timestamp.
303
304 Sets committer timezone to UTC.
305 """
306 env = os.environ.copy()
307 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
308 return RunGit(['commit', '--amend', '-m', message], env=env)
309
310
machenbach@chromium.org45453142015-09-15 08:45:22 +0000311def _get_properties_from_options(options):
312 properties = dict(x.split('=', 1) for x in options.properties)
313 for key, val in properties.iteritems():
314 try:
315 properties[key] = json.loads(val)
316 except ValueError:
317 pass # If a value couldn't be evaluated, treat it as a string.
318 return properties
319
320
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000321def _prefix_master(master):
322 """Convert user-specified master name to full master name.
323
324 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
325 name, while the developers always use shortened master name
326 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
327 function does the conversion for buildbucket migration.
328 """
borenet6c0efe62016-10-19 08:13:29 -0700329 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330 return master
borenet6c0efe62016-10-19 08:13:29 -0700331 return '%s%s' % (MASTER_PREFIX, master)
332
333
334def _unprefix_master(bucket):
335 """Convert bucket name to shortened master name.
336
337 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
338 name, while the developers always use shortened master name
339 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
340 function does the conversion for buildbucket migration.
341 """
342 if bucket.startswith(MASTER_PREFIX):
343 return bucket[len(MASTER_PREFIX):]
344 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345
346
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000347def _buildbucket_retry(operation_name, http, *args, **kwargs):
348 """Retries requests to buildbucket service and returns parsed json content."""
349 try_count = 0
350 while True:
351 response, content = http.request(*args, **kwargs)
352 try:
353 content_json = json.loads(content)
354 except ValueError:
355 content_json = None
356
357 # Buildbucket could return an error even if status==200.
358 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000359 error = content_json.get('error')
360 if error.get('code') == 403:
361 raise BuildbucketResponseException(
362 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000363 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000364 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000365 raise BuildbucketResponseException(msg)
366
367 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700368 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369 raise BuildbucketResponseException(
370 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700371 'Please file bugs at http://crbug.com, '
372 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000373 content)
374 return content_json
375 if response.status < 500 or try_count >= 2:
376 raise httplib2.HttpLib2Error(content)
377
378 # status >= 500 means transient failures.
379 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700380 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000381 try_count += 1
382 assert False, 'unreachable'
383
384
qyearsley1fdfcb62016-10-24 13:22:03 -0700385def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700386 """Returns a dict mapping bucket names to builders and tests,
387 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700388 """
qyearsleydd49f942016-10-28 11:57:22 -0700389 # If no bots are listed, we try to get a set of builders and tests based
390 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 if not options.bot:
392 change = changelist.GetChange(
393 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700394 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700395 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700396 change=change,
397 changed_files=change.LocalPaths(),
398 repository_root=settings.GetRoot(),
399 default_presubmit=None,
400 project=None,
401 verbose=options.verbose,
402 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700403 if masters is None:
404 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100405 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
qyearsley1fdfcb62016-10-24 13:22:03 -0700407 if options.bucket:
408 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700409 if options.master:
410 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700411
qyearsleydd49f942016-10-28 11:57:22 -0700412 # If bots are listed but no master or bucket, then we need to find out
413 # the corresponding master for each bot.
414 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
415 if error_message:
416 option_parser.error(
417 'Tryserver master cannot be found because: %s\n'
418 'Please manually specify the tryserver master, e.g. '
419 '"-m tryserver.chromium.linux".' % error_message)
420 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700421
422
qyearsley123a4682016-10-26 09:12:17 -0700423def _get_bucket_map_for_builders(builders):
424 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700425 map_url = 'https://builders-map.appspot.com/'
426 try:
qyearsley123a4682016-10-26 09:12:17 -0700427 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 except urllib2.URLError as e:
429 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
430 (map_url, e))
431 except ValueError as e:
432 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700433 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700434 return None, 'Failed to build master map.'
435
qyearsley123a4682016-10-26 09:12:17 -0700436 bucket_map = {}
437 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800438 bucket = builders_map.get(builder, {}).get('bucket')
439 if bucket:
440 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700441 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700442
443
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800444def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700445 """Sends a request to Buildbucket to trigger try jobs for a changelist.
446
447 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700448 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700449 changelist: Changelist that the try jobs are associated with.
450 buckets: A nested dict mapping bucket names to builders to tests.
451 options: Command-line options.
452 """
tandriide281ae2016-10-12 06:02:30 -0700453 assert changelist.GetIssue(), 'CL must be uploaded first'
454 codereview_url = changelist.GetCodereviewServer()
455 assert codereview_url, 'CL must be uploaded first'
456 patchset = patchset or changelist.GetMostRecentPatchset()
457 assert patchset, 'CL must be uploaded first'
458
459 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700460 # Cache the buildbucket credentials under the codereview host key, so that
461 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700462 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000463 http = authenticator.authorize(httplib2.Http())
464 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700465
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 buildbucket_put_url = (
467 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000468 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000469 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700470 hostname=codereview_host,
471 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000472 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700473
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700474 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800475 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700476 if options.clobber:
477 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700478 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700479 if extra_properties:
480 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000481
482 batch_req_body = {'builds': []}
483 print_text = []
484 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700485 for bucket, builders_and_tests in sorted(buckets.iteritems()):
486 print_text.append('Bucket: %s' % bucket)
487 master = None
488 if bucket.startswith(MASTER_PREFIX):
489 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000490 for builder, tests in sorted(builders_and_tests.iteritems()):
491 print_text.append(' %s: %s' % (builder, tests))
492 parameters = {
493 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000494 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100495 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000496 'revision': options.revision,
497 }],
tandrii8c5a3532016-11-04 07:52:02 -0700498 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000499 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000500 if 'presubmit' in builder.lower():
501 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000502 if tests:
503 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700504
505 tags = [
506 'builder:%s' % builder,
507 'buildset:%s' % buildset,
508 'user_agent:git_cl_try',
509 ]
510 if master:
511 parameters['properties']['master'] = master
512 tags.append('master:%s' % master)
513
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000514 batch_req_body['builds'].append(
515 {
516 'bucket': bucket,
517 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700519 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000520 }
521 )
522
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000523 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700524 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 http,
526 buildbucket_put_url,
527 'PUT',
528 body=json.dumps(batch_req_body),
529 headers={'Content-Type': 'application/json'}
530 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000531 print_text.append('To see results here, run: git cl try-results')
532 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700533 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000534
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000535
tandrii221ab252016-10-06 08:12:04 -0700536def fetch_try_jobs(auth_config, changelist, buildbucket_host,
537 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700538 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000539
qyearsley53f48a12016-09-01 10:45:13 -0700540 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 """
tandrii221ab252016-10-06 08:12:04 -0700542 assert buildbucket_host
543 assert changelist.GetIssue(), 'CL must be uploaded first'
544 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
545 patchset = patchset or changelist.GetMostRecentPatchset()
546 assert patchset, 'CL must be uploaded first'
547
548 codereview_url = changelist.GetCodereviewServer()
549 codereview_host = urlparse.urlparse(codereview_url).hostname
550 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 if authenticator.has_cached_credentials():
552 http = authenticator.authorize(httplib2.Http())
553 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700554 print('Warning: Some results might be missing because %s' %
555 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700556 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 http = httplib2.Http()
558
559 http.force_exception_to_status_code = True
560
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000561 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700562 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700564 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 params = {'tag': 'buildset:%s' % buildset}
566
567 builds = {}
568 while True:
569 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700570 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700572 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 for build in content.get('builds', []):
574 builds[build['id']] = build
575 if 'next_cursor' in content:
576 params['start_cursor'] = content['next_cursor']
577 else:
578 break
579 return builds
580
581
qyearsleyeab3c042016-08-24 09:18:28 -0700582def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 """Prints nicely result of fetch_try_jobs."""
584 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700585 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000586 return
587
588 # Make a copy, because we'll be modifying builds dictionary.
589 builds = builds.copy()
590 builder_names_cache = {}
591
592 def get_builder(b):
593 try:
594 return builder_names_cache[b['id']]
595 except KeyError:
596 try:
597 parameters = json.loads(b['parameters_json'])
598 name = parameters['builder_name']
599 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700600 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700601 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000602 name = None
603 builder_names_cache[b['id']] = name
604 return name
605
606 def get_bucket(b):
607 bucket = b['bucket']
608 if bucket.startswith('master.'):
609 return bucket[len('master.'):]
610 return bucket
611
612 if options.print_master:
613 name_fmt = '%%-%ds %%-%ds' % (
614 max(len(str(get_bucket(b))) for b in builds.itervalues()),
615 max(len(str(get_builder(b))) for b in builds.itervalues()))
616 def get_name(b):
617 return name_fmt % (get_bucket(b), get_builder(b))
618 else:
619 name_fmt = '%%-%ds' % (
620 max(len(str(get_builder(b))) for b in builds.itervalues()))
621 def get_name(b):
622 return name_fmt % get_builder(b)
623
624 def sort_key(b):
625 return b['status'], b.get('result'), get_name(b), b.get('url')
626
627 def pop(title, f, color=None, **kwargs):
628 """Pop matching builds from `builds` dict and print them."""
629
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000630 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631 colorize = str
632 else:
633 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
634
635 result = []
636 for b in builds.values():
637 if all(b.get(k) == v for k, v in kwargs.iteritems()):
638 builds.pop(b['id'])
639 result.append(b)
640 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700641 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000642 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700643 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000644
645 total = len(builds)
646 pop(status='COMPLETED', result='SUCCESS',
647 title='Successes:', color=Fore.GREEN,
648 f=lambda b: (get_name(b), b.get('url')))
649 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
650 title='Infra Failures:', color=Fore.MAGENTA,
651 f=lambda b: (get_name(b), b.get('url')))
652 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
653 title='Failures:', color=Fore.RED,
654 f=lambda b: (get_name(b), b.get('url')))
655 pop(status='COMPLETED', result='CANCELED',
656 title='Canceled:', color=Fore.MAGENTA,
657 f=lambda b: (get_name(b),))
658 pop(status='COMPLETED', result='FAILURE',
659 failure_reason='INVALID_BUILD_DEFINITION',
660 title='Wrong master/builder name:', color=Fore.MAGENTA,
661 f=lambda b: (get_name(b),))
662 pop(status='COMPLETED', result='FAILURE',
663 title='Other failures:',
664 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
665 pop(status='COMPLETED',
666 title='Other finished:',
667 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
668 pop(status='STARTED',
669 title='Started:', color=Fore.YELLOW,
670 f=lambda b: (get_name(b), b.get('url')))
671 pop(status='SCHEDULED',
672 title='Scheduled:',
673 f=lambda b: (get_name(b), 'id=%s' % b['id']))
674 # The last section is just in case buildbucket API changes OR there is a bug.
675 pop(title='Other:',
676 f=lambda b: (get_name(b), 'id=%s' % b['id']))
677 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700678 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000679
680
Aiden Bennerc08566e2018-10-03 17:52:42 +0000681def _ComputeDiffLineRanges(files, upstream_commit):
682 """Gets the changed line ranges for each file since upstream_commit.
683
684 Parses a git diff on provided files and returns a dict that maps a file name
685 to an ordered list of range tuples in the form (start_line, count).
686 Ranges are in the same format as a git diff.
687 """
688 # If files is empty then diff_output will be a full diff.
689 if len(files) == 0:
690 return {}
691
692 # Take diff and find the line ranges where there are changes.
693 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
694 diff_output = RunGit(diff_cmd)
695
696 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
697 # 2 capture groups
698 # 0 == fname of diff file
699 # 1 == 'diff_start,diff_count' or 'diff_start'
700 # will match each of
701 # diff --git a/foo.foo b/foo.py
702 # @@ -12,2 +14,3 @@
703 # @@ -12,2 +17 @@
704 # running re.findall on the above string with pattern will give
705 # [('foo.py', ''), ('', '14,3'), ('', '17')]
706
707 curr_file = None
708 line_diffs = {}
709 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
710 if match[0] != '':
711 # Will match the second filename in diff --git a/a.py b/b.py.
712 curr_file = match[0]
713 line_diffs[curr_file] = []
714 else:
715 # Matches +14,3
716 if ',' in match[1]:
717 diff_start, diff_count = match[1].split(',')
718 else:
719 # Single line changes are of the form +12 instead of +12,1.
720 diff_start = match[1]
721 diff_count = 1
722
723 diff_start = int(diff_start)
724 diff_count = int(diff_count)
725
726 # If diff_count == 0 this is a removal we can ignore.
727 line_diffs[curr_file].append((diff_start, diff_count))
728
729 return line_diffs
730
731
732def _FindYapfConfigFile(fpath,
733 yapf_config_cache,
734 top_dir=None,
735 default_style=None):
736 """Checks if a yapf file is in any parent directory of fpath until top_dir.
737
738 Recursively checks parent directories to find yapf file
739 and if no yapf file is found returns default_style.
740 Uses yapf_config_cache as a cache for previously found files.
741 """
742 # Return result if we've already computed it.
743 if fpath in yapf_config_cache:
744 return yapf_config_cache[fpath]
745
746 # Check if there is a style file in the current directory.
747 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
748 dirname = os.path.dirname(fpath)
749 if os.path.isfile(yapf_file):
750 ret = yapf_file
751 elif fpath == top_dir or dirname == fpath:
752 # If we're at the top level directory, or if we're at root
753 # use the chromium default yapf style.
754 ret = default_style
755 else:
756 # Otherwise recurse on the current directory.
757 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
758 default_style)
759 yapf_config_cache[fpath] = ret
760 return ret
761
762
qyearsley53f48a12016-09-01 10:45:13 -0700763def write_try_results_json(output_file, builds):
764 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
765
766 The input |builds| dict is assumed to be generated by Buildbucket.
767 Buildbucket documentation: http://goo.gl/G0s101
768 """
769
770 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800771 """Extracts some of the information from one build dict."""
772 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700773 return {
774 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700775 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800776 'builder_name': parameters.get('builder_name'),
777 'created_ts': build.get('created_ts'),
778 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700779 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800780 'result': build.get('result'),
781 'status': build.get('status'),
782 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700783 'url': build.get('url'),
784 }
785
786 converted = []
787 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000788 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700789 write_json(output_file, converted)
790
791
Aaron Gable13101a62018-02-09 13:20:41 -0800792def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000793 """Prints statistics about the change to the user."""
794 # --no-ext-diff is broken in some versions of Git, so try to work around
795 # this by overriding the environment (but there is still a problem if the
796 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000797 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000798 if 'GIT_EXTERNAL_DIFF' in env:
799 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000800
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000801 try:
802 stdout = sys.stdout.fileno()
803 except AttributeError:
804 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000805 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800806 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000807 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000808
809
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000810class BuildbucketResponseException(Exception):
811 pass
812
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814class Settings(object):
815 def __init__(self):
816 self.default_server = None
817 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000818 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 self.tree_status_url = None
820 self.viewvc_url = None
821 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000822 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000823 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000824 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000825 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000826 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000827 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828
829 def LazyUpdateIfNeeded(self):
830 """Updates the settings from a codereview.settings file, if available."""
831 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000832 # The only value that actually changes the behavior is
833 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000834 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000835 error_ok=True
836 ).strip().lower()
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000839 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840 LoadCodereviewSettingsFromFile(cr_settings_file)
841 self.updated = True
842
843 def GetDefaultServerUrl(self, error_ok=False):
844 if not self.default_server:
845 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000846 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 if error_ok:
849 return self.default_server
850 if not self.default_server:
851 error_message = ('Could not find settings file. You must configure '
852 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000853 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000854 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 return self.default_server
856
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000857 @staticmethod
858 def GetRelativeRoot():
859 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000862 if self.root is None:
863 self.root = os.path.abspath(self.GetRelativeRoot())
864 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000866 def GetGitMirror(self, remote='origin'):
867 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000868 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000869 if not os.path.isdir(local_url):
870 return None
871 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
872 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100873 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100874 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000875 if mirror.exists():
876 return mirror
877 return None
878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 def GetTreeStatusUrl(self, error_ok=False):
880 if not self.tree_status_url:
881 error_message = ('You must configure your tree status URL by running '
882 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000883 self.tree_status_url = self._GetRietveldConfig(
884 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 return self.tree_status_url
886
887 def GetViewVCUrl(self):
888 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000889 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 return self.viewvc_url
891
rmistry@google.com90752582014-01-14 21:04:50 +0000892 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000893 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000894
rmistry@google.com78948ed2015-07-08 23:09:57 +0000895 def GetIsSkipDependencyUpload(self, branch_name):
896 """Returns true if specified branch should skip dep uploads."""
897 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
898 error_ok=True)
899
rmistry@google.com5626a922015-02-26 14:03:30 +0000900 def GetRunPostUploadHook(self):
901 run_post_upload_hook = self._GetRietveldConfig(
902 'run-post-upload-hook', error_ok=True)
903 return run_post_upload_hook == "True"
904
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000905 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000906 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000907
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000908 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000909 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000910
ukai@chromium.orge8077812012-02-03 03:41:46 +0000911 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700912 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000913 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700914 self.is_gerrit = (
915 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000916 return self.is_gerrit
917
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000918 def GetSquashGerritUploads(self):
919 """Return true if uploads to Gerrit should be squashed by default."""
920 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700921 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
922 if self.squash_gerrit_uploads is None:
923 # Default is squash now (http://crbug.com/611892#c23).
924 self.squash_gerrit_uploads = not (
925 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
926 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000927 return self.squash_gerrit_uploads
928
tandriia60502f2016-06-20 02:01:53 -0700929 def GetSquashGerritUploadsOverride(self):
930 """Return True or False if codereview.settings should be overridden.
931
932 Returns None if no override has been defined.
933 """
934 # See also http://crbug.com/611892#c23
935 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
936 error_ok=True).strip()
937 if result == 'true':
938 return True
939 if result == 'false':
940 return False
941 return None
942
tandrii@chromium.org28253532016-04-14 13:46:56 +0000943 def GetGerritSkipEnsureAuthenticated(self):
944 """Return True if EnsureAuthenticated should not be done for Gerrit
945 uploads."""
946 if self.gerrit_skip_ensure_authenticated is None:
947 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000948 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000949 error_ok=True).strip() == 'true')
950 return self.gerrit_skip_ensure_authenticated
951
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000952 def GetGitEditor(self):
953 """Return the editor specified in the git config, or None if none is."""
954 if self.git_editor is None:
955 self.git_editor = self._GetConfig('core.editor', error_ok=True)
956 return self.git_editor or None
957
thestig@chromium.org44202a22014-03-11 19:22:18 +0000958 def GetLintRegex(self):
959 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
960 DEFAULT_LINT_REGEX)
961
962 def GetLintIgnoreRegex(self):
963 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
964 DEFAULT_LINT_IGNORE_REGEX)
965
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000966 def GetProject(self):
967 if not self.project:
968 self.project = self._GetRietveldConfig('project', error_ok=True)
969 return self.project
970
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000971 def _GetRietveldConfig(self, param, **kwargs):
972 return self._GetConfig('rietveld.' + param, **kwargs)
973
rmistry@google.com78948ed2015-07-08 23:09:57 +0000974 def _GetBranchConfig(self, branch_name, param, **kwargs):
975 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
976
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 def _GetConfig(self, param, **kwargs):
978 self.LazyUpdateIfNeeded()
979 return RunGit(['config', param], **kwargs).strip()
980
981
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100982@contextlib.contextmanager
983def _get_gerrit_project_config_file(remote_url):
984 """Context manager to fetch and store Gerrit's project.config from
985 refs/meta/config branch and store it in temp file.
986
987 Provides a temporary filename or None if there was error.
988 """
989 error, _ = RunGitWithCode([
990 'fetch', remote_url,
991 '+refs/meta/config:refs/git_cl/meta/config'])
992 if error:
993 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700994 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100995 (remote_url, error))
996 yield None
997 return
998
999 error, project_config_data = RunGitWithCode(
1000 ['show', 'refs/git_cl/meta/config:project.config'])
1001 if error:
1002 print('WARNING: project.config file not found')
1003 yield None
1004 return
1005
1006 with gclient_utils.temporary_directory() as tempdir:
1007 project_config_file = os.path.join(tempdir, 'project.config')
1008 gclient_utils.FileWrite(project_config_file, project_config_data)
1009 yield project_config_file
1010
1011
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012def ShortBranchName(branch):
1013 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001014 return branch.replace('refs/heads/', '', 1)
1015
1016
1017def GetCurrentBranchRef():
1018 """Returns branch ref (e.g., refs/heads/master) or None."""
1019 return RunGit(['symbolic-ref', 'HEAD'],
1020 stderr=subprocess2.VOID, error_ok=True).strip() or None
1021
1022
1023def GetCurrentBranch():
1024 """Returns current branch or None.
1025
1026 For refs/heads/* branches, returns just last part. For others, full ref.
1027 """
1028 branchref = GetCurrentBranchRef()
1029 if branchref:
1030 return ShortBranchName(branchref)
1031 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032
1033
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001034class _CQState(object):
1035 """Enum for states of CL with respect to Commit Queue."""
1036 NONE = 'none'
1037 DRY_RUN = 'dry_run'
1038 COMMIT = 'commit'
1039
1040 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1041
1042
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001044 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045 self.issue = issue
1046 self.patchset = patchset
1047 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001048 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001049 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050
1051 @property
1052 def valid(self):
1053 return self.issue is not None
1054
1055
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001056def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001057 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1058 fail_result = _ParsedIssueNumberArgument()
1059
1060 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001061 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001062 if not arg.startswith('http'):
1063 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001064
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001065 url = gclient_utils.UpgradeToHttps(arg)
1066 try:
1067 parsed_url = urlparse.urlparse(url)
1068 except ValueError:
1069 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001070
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001071 if codereview is not None:
1072 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1073 return parsed or fail_result
1074
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001075 results = {}
1076 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1077 parsed = cls.ParseIssueURL(parsed_url)
1078 if parsed is not None:
1079 results[name] = parsed
1080
1081 if not results:
1082 return fail_result
1083 if len(results) == 1:
1084 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001085
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001086 return results['gerrit']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001087
1088
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001089def _create_description_from_log(args):
1090 """Pulls out the commit log to use as a base for the CL description."""
1091 log_args = []
1092 if len(args) == 1 and not args[0].endswith('.'):
1093 log_args = [args[0] + '..']
1094 elif len(args) == 1 and args[0].endswith('...'):
1095 log_args = [args[0][:-1]]
1096 elif len(args) == 2:
1097 log_args = [args[0] + '..' + args[1]]
1098 else:
1099 log_args = args[:] # Hope for the best!
1100 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1101
1102
Aaron Gablea45ee112016-11-22 15:14:38 -08001103class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001104 def __init__(self, issue, url):
1105 self.issue = issue
1106 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001107 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001108
1109 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001110 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001111 self.issue, self.url)
1112
1113
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001114_CommentSummary = collections.namedtuple(
1115 '_CommentSummary', ['date', 'message', 'sender',
1116 # TODO(tandrii): these two aren't known in Gerrit.
1117 'approval', 'disapproval'])
1118
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121 """Changelist works with one changelist in local branch.
1122
1123 Supports two codereview backends: Rietveld or Gerrit, selected at object
1124 creation.
1125
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001126 Notes:
1127 * Not safe for concurrent multi-{thread,process} use.
1128 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001129 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001130 """
1131
1132 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1133 """Create a new ChangeList instance.
1134
1135 If issue is given, the codereview must be given too.
1136
1137 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1138 Otherwise, it's decided based on current configuration of the local branch,
1139 with default being 'rietveld' for backwards compatibility.
1140 See _load_codereview_impl for more details.
1141
1142 **kwargs will be passed directly to codereview implementation.
1143 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001145 global settings
1146 if not settings:
1147 # Happens when git_cl.py is used as a utility library.
1148 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149
1150 if issue:
1151 assert codereview, 'codereview must be known, if issue is known'
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.branchref = branchref
1154 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001155 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 self.branch = ShortBranchName(self.branchref)
1157 else:
1158 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 self.lookedup_issue = False
1161 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 self.has_description = False
1163 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001164 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001166 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001167 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001168 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001169 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001170
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001171 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001172 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001174 assert self._codereview_impl
1175 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176
1177 def _load_codereview_impl(self, codereview=None, **kwargs):
1178 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001179 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1180 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1181 self._codereview = codereview
1182 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 return
1184
1185 # Automatic selection based on issue number set for a current branch.
1186 # Rietveld takes precedence over Gerrit.
1187 assert not self.issue
1188 # Whether we find issue or not, we are doing the lookup.
1189 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001190 if self.GetBranch():
1191 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1192 issue = _git_get_branch_config_value(
1193 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1194 if issue:
1195 self._codereview = codereview
1196 self._codereview_impl = cls(self, **kwargs)
1197 self.issue = int(issue)
1198 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001199
1200 # No issue is set for this branch, so decide based on repo-wide settings.
1201 return self._load_codereview_impl(
1202 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1203 **kwargs)
1204
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001205 def IsGerrit(self):
1206 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001207
1208 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001209 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001210
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001211 The return value is a string suitable for passing to git cl with the --cc
1212 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001213 """
1214 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001215 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001216 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001217 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1218 return self.cc
1219
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001220 def GetCCListWithoutDefault(self):
1221 """Return the users cc'd on this CL excluding default ones."""
1222 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001223 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001224 return self.cc
1225
Daniel Cheng7227d212017-11-17 08:12:37 -08001226 def ExtendCC(self, more_cc):
1227 """Extends the list of users to cc on this CL based on the changed files."""
1228 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229
1230 def GetBranch(self):
1231 """Returns the short branch name, e.g. 'master'."""
1232 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001233 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001234 if not branchref:
1235 return None
1236 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 self.branch = ShortBranchName(self.branchref)
1238 return self.branch
1239
1240 def GetBranchRef(self):
1241 """Returns the full branch name, e.g. 'refs/heads/master'."""
1242 self.GetBranch() # Poke the lazy loader.
1243 return self.branchref
1244
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001245 def ClearBranch(self):
1246 """Clears cached branch data of this object."""
1247 self.branch = self.branchref = None
1248
tandrii5d48c322016-08-18 16:19:37 -07001249 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1250 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1251 kwargs['branch'] = self.GetBranch()
1252 return _git_get_branch_config_value(key, default, **kwargs)
1253
1254 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1255 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1256 assert self.GetBranch(), (
1257 'this CL must have an associated branch to %sset %s%s' %
1258 ('un' if value is None else '',
1259 key,
1260 '' if value is None else ' to %r' % value))
1261 kwargs['branch'] = self.GetBranch()
1262 return _git_set_branch_config_value(key, value, **kwargs)
1263
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001264 @staticmethod
1265 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001266 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 e.g. 'origin', 'refs/heads/master'
1268 """
1269 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001270 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001273 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001275 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1276 error_ok=True).strip()
1277 if upstream_branch:
1278 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001280 # Else, try to guess the origin remote.
1281 remote_branches = RunGit(['branch', '-r']).split()
1282 if 'origin/master' in remote_branches:
1283 # Fall back on origin/master if it exits.
1284 remote = 'origin'
1285 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001287 DieWithError(
1288 'Unable to determine default branch to diff against.\n'
1289 'Either pass complete "git diff"-style arguments, like\n'
1290 ' git cl upload origin/master\n'
1291 'or verify this branch is set up to track another \n'
1292 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293
1294 return remote, upstream_branch
1295
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001296 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001297 upstream_branch = self.GetUpstreamBranch()
1298 if not BranchExists(upstream_branch):
1299 DieWithError('The upstream for the current branch (%s) does not exist '
1300 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001301 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001302 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 def GetUpstreamBranch(self):
1305 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001306 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001308 upstream_branch = upstream_branch.replace('refs/heads/',
1309 'refs/remotes/%s/' % remote)
1310 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1311 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 self.upstream_branch = upstream_branch
1313 return self.upstream_branch
1314
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001316 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 remote, branch = None, self.GetBranch()
1318 seen_branches = set()
1319 while branch not in seen_branches:
1320 seen_branches.add(branch)
1321 remote, branch = self.FetchUpstreamTuple(branch)
1322 branch = ShortBranchName(branch)
1323 if remote != '.' or branch.startswith('refs/remotes'):
1324 break
1325 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001326 remotes = RunGit(['remote'], error_ok=True).split()
1327 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001329 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001331 logging.warn('Could not determine which remote this change is '
1332 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001333 else:
1334 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001335 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001336 branch = 'HEAD'
1337 if branch.startswith('refs/remotes'):
1338 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001339 elif branch.startswith('refs/branch-heads/'):
1340 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 else:
1342 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001343 return self._remote
1344
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 def GitSanityChecks(self, upstream_git_obj):
1346 """Checks git repo status and ensures diff is from local commits."""
1347
sbc@chromium.org79706062015-01-14 21:18:12 +00001348 if upstream_git_obj is None:
1349 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001350 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001351 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001352 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001353 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001354 return False
1355
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001356 # Verify the commit we're diffing against is in our current branch.
1357 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1358 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1359 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001360 print('ERROR: %s is not in the current branch. You may need to rebase '
1361 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001362 return False
1363
1364 # List the commits inside the diff, and verify they are all local.
1365 commits_in_diff = RunGit(
1366 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1367 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1368 remote_branch = remote_branch.strip()
1369 if code != 0:
1370 _, remote_branch = self.GetRemoteBranch()
1371
1372 commits_in_remote = RunGit(
1373 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1374
1375 common_commits = set(commits_in_diff) & set(commits_in_remote)
1376 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001377 print('ERROR: Your diff contains %d commits already in %s.\n'
1378 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1379 'the diff. If you are using a custom git flow, you can override'
1380 ' the reference used for this check with "git config '
1381 'gitcl.remotebranch <git-ref>".' % (
1382 len(common_commits), remote_branch, upstream_git_obj),
1383 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 return False
1385 return True
1386
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001387 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001388 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001389
1390 Returns None if it is not set.
1391 """
tandrii5d48c322016-08-18 16:19:37 -07001392 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 def GetRemoteUrl(self):
1395 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1396
1397 Returns None if there is no remote.
1398 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001399 is_cached, value = self._cached_remote_url
1400 if is_cached:
1401 return value
1402
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001403 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001404 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1405
1406 # If URL is pointing to a local directory, it is probably a git cache.
1407 if os.path.isdir(url):
1408 url = RunGit(['config', 'remote.%s.url' % remote],
1409 error_ok=True,
1410 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001411 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001412 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001414 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001415 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001416 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001417 self.issue = self._GitGetBranchConfigValue(
1418 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001419 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 return self.issue
1421
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 def GetIssueURL(self):
1423 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 issue = self.GetIssue()
1425 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001426 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001427 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001429 def GetDescription(self, pretty=False, force=False):
1430 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001432 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 self.has_description = True
1434 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001435 # Set width to 72 columns + 2 space indent.
1436 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001438 lines = self.description.splitlines()
1439 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 return self.description
1441
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001442 def GetDescriptionFooters(self):
1443 """Returns (non_footer_lines, footers) for the commit message.
1444
1445 Returns:
1446 non_footer_lines (list(str)) - Simple list of description lines without
1447 any footer. The lines do not contain newlines, nor does the list contain
1448 the empty line between the message and the footers.
1449 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1450 [("Change-Id", "Ideadbeef...."), ...]
1451 """
1452 raw_description = self.GetDescription()
1453 msg_lines, _, footers = git_footers.split_footers(raw_description)
1454 if footers:
1455 msg_lines = msg_lines[:len(msg_lines)-1]
1456 return msg_lines, footers
1457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001459 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001460 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001461 self.patchset = self._GitGetBranchConfigValue(
1462 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464 return self.patchset
1465
1466 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001467 """Set this branch's patchset. If patchset=0, clears the patchset."""
1468 assert self.GetBranch()
1469 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001470 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001471 else:
1472 self.patchset = int(patchset)
1473 self._GitSetBranchConfigValue(
1474 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001476 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001477 """Set this branch's issue. If issue isn't given, clears the issue."""
1478 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001480 issue = int(issue)
1481 self._GitSetBranchConfigValue(
1482 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001483 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001484 codereview_server = self._codereview_impl.GetCodereviewServer()
1485 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001486 self._GitSetBranchConfigValue(
1487 self._codereview_impl.CodereviewServerConfigKey(),
1488 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 else:
tandrii5d48c322016-08-18 16:19:37 -07001490 # Reset all of these just to be clean.
1491 reset_suffixes = [
1492 'last-upload-hash',
1493 self._codereview_impl.IssueConfigKey(),
1494 self._codereview_impl.PatchsetConfigKey(),
1495 self._codereview_impl.CodereviewServerConfigKey(),
1496 ] + self._PostUnsetIssueProperties()
1497 for prop in reset_suffixes:
1498 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001499 msg = RunGit(['log', '-1', '--format=%B']).strip()
1500 if msg and git_footers.get_footer_change_id(msg):
1501 print('WARNING: The change patched into this branch has a Change-Id. '
1502 'Removing it.')
1503 RunGit(['commit', '--amend', '-m',
1504 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001505 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001506 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507
dnjba1b0f32016-09-02 12:37:42 -07001508 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001509 if not self.GitSanityChecks(upstream_branch):
1510 DieWithError('\nGit sanity check failure')
1511
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001513 if not root:
1514 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001515 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001516
1517 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001518 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001519 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001520 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001521 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001522 except subprocess2.CalledProcessError:
1523 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001524 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001525 'This branch probably doesn\'t exist anymore. To reset the\n'
1526 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001527 ' git branch --set-upstream-to origin/master %s\n'
1528 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001529 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001530
maruel@chromium.org52424302012-08-29 15:14:30 +00001531 issue = self.GetIssue()
1532 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001533 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001534 description = self.GetDescription()
1535 else:
1536 # If the change was never uploaded, use the log messages of all commits
1537 # up to the branch point, as git cl upload will prefill the description
1538 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001539 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1540 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001541
1542 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001543 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001544 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001545 name,
1546 description,
1547 absroot,
1548 files,
1549 issue,
1550 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001551 author,
1552 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001553
dsansomee2d6fd92016-09-08 00:10:47 -07001554 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001555 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001556 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001557 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001558
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001559 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1560 """Sets the description for this CL remotely.
1561
1562 You can get description_lines and footers with GetDescriptionFooters.
1563
1564 Args:
1565 description_lines (list(str)) - List of CL description lines without
1566 newline characters.
1567 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1568 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1569 `List-Of-Tokens`). It will be case-normalized so that each token is
1570 title-cased.
1571 """
1572 new_description = '\n'.join(description_lines)
1573 if footers:
1574 new_description += '\n'
1575 for k, v in footers:
1576 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1577 if not git_footers.FOOTER_PATTERN.match(foot):
1578 raise ValueError('Invalid footer %r' % foot)
1579 new_description += foot + '\n'
1580 self.UpdateDescription(new_description, force)
1581
Edward Lesmes8e282792018-04-03 18:50:29 -04001582 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1584 try:
1585 return presubmit_support.DoPresubmitChecks(change, committing,
1586 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1587 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001588 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1589 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001590 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001591 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1594 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001595 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1596 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001597 else:
1598 # Assume url.
1599 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1600 urlparse.urlparse(issue_arg))
1601 if not parsed_issue_arg or not parsed_issue_arg.valid:
1602 DieWithError('Failed to parse issue argument "%s". '
1603 'Must be an issue number or a valid URL.' % issue_arg)
1604 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001605 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001606
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001607 def CMDUpload(self, options, git_diff_args, orig_args):
1608 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001609 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001610 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001611 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001612 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613 else:
1614 if self.GetBranch() is None:
1615 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1616
1617 # Default to diffing against common ancestor of upstream branch
1618 base_branch = self.GetCommonAncestorWithUpstream()
1619 git_diff_args = [base_branch, 'HEAD']
1620
Aaron Gablec4c40d12017-05-22 11:49:53 -07001621
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001622 # Fast best-effort checks to abort before running potentially
1623 # expensive hooks if uploading is likely to fail anyway. Passing these
1624 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001625 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001626 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627
1628 # Apply watchlists on upload.
1629 change = self.GetChange(base_branch, None)
1630 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1631 files = [f.LocalPath() for f in change.AffectedFiles()]
1632 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001633 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001634
1635 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001636 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637 # Set the reviewer list now so that presubmit checks can access it.
1638 change_description = ChangeDescription(change.FullDescriptionText())
1639 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001640 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001641 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 change)
1643 change.SetDescriptionText(change_description.description)
1644 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001645 may_prompt=not options.force,
1646 verbose=options.verbose,
1647 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001648 if not hook_results.should_continue():
1649 return 1
1650 if not options.reviewers and hook_results.reviewers:
1651 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001652 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001653
Aaron Gable13101a62018-02-09 13:20:41 -08001654 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001655 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001656 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001657 _git_set_branch_config_value('last-upload-hash',
1658 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001659 # Run post upload hooks, if specified.
1660 if settings.GetRunPostUploadHook():
1661 presubmit_support.DoPostUploadExecuter(
1662 change,
1663 self,
1664 settings.GetRoot(),
1665 options.verbose,
1666 sys.stdout)
1667
1668 # Upload all dependencies if specified.
1669 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001670 print()
1671 print('--dependencies has been specified.')
1672 print('All dependent local branches will be re-uploaded.')
1673 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001674 # Remove the dependencies flag from args so that we do not end up in a
1675 # loop.
1676 orig_args.remove('--dependencies')
1677 ret = upload_branch_deps(self, orig_args)
1678 return ret
1679
Ravi Mistry31e7d562018-04-02 12:53:57 -04001680 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1681 """Sets labels on the change based on the provided flags.
1682
1683 Sets labels if issue is already uploaded and known, else returns without
1684 doing anything.
1685
1686 Args:
1687 enable_auto_submit: Sets Auto-Submit+1 on the change.
1688 use_commit_queue: Sets Commit-Queue+2 on the change.
1689 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1690 both use_commit_queue and cq_dry_run are true.
1691 """
1692 if not self.GetIssue():
1693 return
1694 try:
1695 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1696 cq_dry_run)
1697 return 0
1698 except KeyboardInterrupt:
1699 raise
1700 except:
1701 labels = []
1702 if enable_auto_submit:
1703 labels.append('Auto-Submit')
1704 if use_commit_queue or cq_dry_run:
1705 labels.append('Commit-Queue')
1706 print('WARNING: Failed to set label(s) on your change: %s\n'
1707 'Either:\n'
1708 ' * Your project does not have the above label(s),\n'
1709 ' * You don\'t have permission to set the above label(s),\n'
1710 ' * There\'s a bug in this code (see stack trace below).\n' %
1711 (', '.join(labels)))
1712 # Still raise exception so that stack trace is printed.
1713 raise
1714
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001715 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001716 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001717
1718 Issue must have been already uploaded and known.
1719 """
1720 assert new_state in _CQState.ALL_STATES
1721 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001722 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001723 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001724 return 0
1725 except KeyboardInterrupt:
1726 raise
1727 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001728 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001729 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001730 ' * Your project has no CQ,\n'
1731 ' * You don\'t have permission to change the CQ state,\n'
1732 ' * There\'s a bug in this code (see stack trace below).\n'
1733 'Consider specifying which bots to trigger manually or asking your '
1734 'project owners for permissions or contacting Chrome Infra at:\n'
1735 'https://www.chromium.org/infra\n\n' %
1736 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001737 # Still raise exception so that stack trace is printed.
1738 raise
1739
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 # Forward methods to codereview specific implementation.
1741
Aaron Gable636b13f2017-07-14 10:42:48 -07001742 def AddComment(self, message, publish=None):
1743 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001744
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001745 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001746 """Returns list of _CommentSummary for each comment.
1747
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001748 args:
1749 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001750 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001751 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001752
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 def CloseIssue(self):
1754 return self._codereview_impl.CloseIssue()
1755
1756 def GetStatus(self):
1757 return self._codereview_impl.GetStatus()
1758
1759 def GetCodereviewServer(self):
1760 return self._codereview_impl.GetCodereviewServer()
1761
tandriide281ae2016-10-12 06:02:30 -07001762 def GetIssueOwner(self):
1763 """Get owner from codereview, which may differ from this checkout."""
1764 return self._codereview_impl.GetIssueOwner()
1765
Edward Lemur707d70b2018-02-07 00:50:14 +01001766 def GetReviewers(self):
1767 return self._codereview_impl.GetReviewers()
1768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetMostRecentPatchset(self):
1770 return self._codereview_impl.GetMostRecentPatchset()
1771
tandriide281ae2016-10-12 06:02:30 -07001772 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001773 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001774 return self._codereview_impl.CannotTriggerTryJobReason()
1775
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001776 def GetTryJobProperties(self, patchset=None):
1777 """Returns dictionary of properties to launch try job."""
1778 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001779
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001780 def __getattr__(self, attr):
1781 # This is because lots of untested code accesses Rietveld-specific stuff
1782 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001783 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001784 # Note that child method defines __getattr__ as well, and forwards it here,
1785 # because _RietveldChangelistImpl is not cleaned up yet, and given
1786 # deprecation of Rietveld, it should probably be just removed.
1787 # Until that time, avoid infinite recursion by bypassing __getattr__
1788 # of implementation class.
1789 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001790
1791
1792class _ChangelistCodereviewBase(object):
1793 """Abstract base class encapsulating codereview specifics of a changelist."""
1794 def __init__(self, changelist):
1795 self._changelist = changelist # instance of Changelist
1796
1797 def __getattr__(self, attr):
1798 # Forward methods to changelist.
1799 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1800 # _RietveldChangelistImpl to avoid this hack?
1801 return getattr(self._changelist, attr)
1802
1803 def GetStatus(self):
1804 """Apply a rough heuristic to give a simple summary of an issue's review
1805 or CQ status, assuming adherence to a common workflow.
1806
1807 Returns None if no issue for this branch, or specific string keywords.
1808 """
1809 raise NotImplementedError()
1810
1811 def GetCodereviewServer(self):
1812 """Returns server URL without end slash, like "https://codereview.com"."""
1813 raise NotImplementedError()
1814
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001815 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001816 """Fetches and returns description from the codereview server."""
1817 raise NotImplementedError()
1818
tandrii5d48c322016-08-18 16:19:37 -07001819 @classmethod
1820 def IssueConfigKey(cls):
1821 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001822 raise NotImplementedError()
1823
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001824 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001825 def PatchsetConfigKey(cls):
1826 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 raise NotImplementedError()
1828
tandrii5d48c322016-08-18 16:19:37 -07001829 @classmethod
1830 def CodereviewServerConfigKey(cls):
1831 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 raise NotImplementedError()
1833
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001834 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001835 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001836 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001837
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001838 def GetGerritObjForPresubmit(self):
1839 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1840 return None
1841
dsansomee2d6fd92016-09-08 00:10:47 -07001842 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001843 """Update the description on codereview site."""
1844 raise NotImplementedError()
1845
Aaron Gable636b13f2017-07-14 10:42:48 -07001846 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001847 """Posts a comment to the codereview site."""
1848 raise NotImplementedError()
1849
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001850 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001851 raise NotImplementedError()
1852
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001853 def CloseIssue(self):
1854 """Closes the issue."""
1855 raise NotImplementedError()
1856
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001857 def GetMostRecentPatchset(self):
1858 """Returns the most recent patchset number from the codereview site."""
1859 raise NotImplementedError()
1860
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001861 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001862 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001863 """Fetches and applies the issue.
1864
1865 Arguments:
1866 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1867 reject: if True, reject the failed patch instead of switching to 3-way
1868 merge. Rietveld only.
1869 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1870 only.
1871 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001872 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001873 """
1874 raise NotImplementedError()
1875
1876 @staticmethod
1877 def ParseIssueURL(parsed_url):
1878 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1879 failed."""
1880 raise NotImplementedError()
1881
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001882 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001883 """Best effort check that user is authenticated with codereview server.
1884
1885 Arguments:
1886 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001887 refresh: whether to attempt to refresh credentials. Ignored if not
1888 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001889 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001890 raise NotImplementedError()
1891
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001892 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001893 """Best effort check that uploading isn't supposed to fail for predictable
1894 reasons.
1895
1896 This method should raise informative exception if uploading shouldn't
1897 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001898
1899 Arguments:
1900 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001901 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001902 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001903
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001904 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001905 """Uploads a change to codereview."""
1906 raise NotImplementedError()
1907
Ravi Mistry31e7d562018-04-02 12:53:57 -04001908 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1909 """Sets labels on the change based on the provided flags.
1910
1911 Issue must have been already uploaded and known.
1912 """
1913 raise NotImplementedError()
1914
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001915 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001916 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001917
1918 Issue must have been already uploaded and known.
1919 """
1920 raise NotImplementedError()
1921
tandriie113dfd2016-10-11 10:20:12 -07001922 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001923 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001924 raise NotImplementedError()
1925
tandriide281ae2016-10-12 06:02:30 -07001926 def GetIssueOwner(self):
1927 raise NotImplementedError()
1928
Edward Lemur707d70b2018-02-07 00:50:14 +01001929 def GetReviewers(self):
1930 raise NotImplementedError()
1931
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001932 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001933 raise NotImplementedError()
1934
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935
1936class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001937
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001938 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939 super(_RietveldChangelistImpl, self).__init__(changelist)
1940 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001941 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001942 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001943
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001944 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001945 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001946 self._props = None
1947 self._rpc_server = None
1948
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001949 def GetCodereviewServer(self):
1950 if not self._rietveld_server:
1951 # If we're on a branch then get the server potentially associated
1952 # with that branch.
1953 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001954 self._rietveld_server = gclient_utils.UpgradeToHttps(
1955 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 if not self._rietveld_server:
1957 self._rietveld_server = settings.GetDefaultServerUrl()
1958 return self._rietveld_server
1959
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001960 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001961 # No checks for Rietveld because we are deprecating Rietveld.
1962 pass
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001963
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001964 def EnsureCanUploadPatchset(self, force):
1965 # No checks for Rietveld because we are deprecating Rietveld.
1966 pass
1967
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001968 def FetchDescription(self, force=False):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001969 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001970
1971 def GetMostRecentPatchset(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001972 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001974 def GetIssueProperties(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001975 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001976
tandriie113dfd2016-10-11 10:20:12 -07001977 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001978 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07001979
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001980 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001981 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07001982
tandriide281ae2016-10-12 06:02:30 -07001983 def GetIssueOwner(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001984 raise NotImplementedError()
tandriide281ae2016-10-12 06:02:30 -07001985
Edward Lemur707d70b2018-02-07 00:50:14 +01001986 def GetReviewers(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001987 raise NotImplementedError()
Edward Lemur707d70b2018-02-07 00:50:14 +01001988
Aaron Gable636b13f2017-07-14 10:42:48 -07001989 def AddComment(self, message, publish=None):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001990 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001991
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001992 def GetCommentsSummary(self, readable=True):
1993 raise NotImplementedError()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001994
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001995 def GetStatus(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001996 print(
1997 'WARNING! Rietveld is no longer supported.\n'
1998 '\n'
1999 'If you have old branches in your checkout, please archive/delete them.\n'
2000 ' $ git cl archive --help\n'
2001 '\n'
2002 'See also PSA https://groups.google.com/a/chromium.org/'
2003 'forum/#!topic/infra-dev/2DIVzM2wseo\n')
2004 return 'rietveld-not-supported'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002005
dsansomee2d6fd92016-09-08 00:10:47 -07002006 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00002007 raise NotImplementedError()
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002008
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 def CloseIssue(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00002010 raise NotImplementedError()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002012 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002013 return self.SetFlags({flag: value})
2014
2015 def SetFlags(self, flags):
2016 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002017 """
phajdan.jr68598232016-08-10 03:28:28 -07002018 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002019 try:
tandrii4b233bd2016-07-06 03:50:29 -07002020 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002021 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002022 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002023 if e.code == 404:
2024 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2025 if e.code == 403:
2026 DieWithError(
2027 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002028 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002029 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002031 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032 """Returns an upload.RpcServer() to access this review's rietveld instance.
2033 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002034 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002035 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002036 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002037 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002038 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002040 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002041 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002042 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
tandrii5d48c322016-08-18 16:19:37 -07002044 @classmethod
2045 def PatchsetConfigKey(cls):
2046 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047
tandrii5d48c322016-08-18 16:19:37 -07002048 @classmethod
2049 def CodereviewServerConfigKey(cls):
2050 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002051
Ravi Mistry31e7d562018-04-02 12:53:57 -04002052 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2053 raise NotImplementedError()
2054
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002055 def SetCQState(self, new_state):
2056 props = self.GetIssueProperties()
2057 if props.get('private'):
2058 DieWithError('Cannot set-commit on private issue')
2059
2060 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002061 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002062 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002063 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002064 else:
tandrii4b233bd2016-07-06 03:50:29 -07002065 assert new_state == _CQState.DRY_RUN
2066 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002067
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002068 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002069 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002070 # PatchIssue should never be called with a dirty tree. It is up to the
2071 # caller to check this, but just in case we assert here since the
2072 # consequences of the caller not checking this could be dire.
2073 assert(not git_common.is_dirty_git_tree('apply'))
2074 assert(parsed_issue_arg.valid)
2075 self._changelist.issue = parsed_issue_arg.issue
2076 if parsed_issue_arg.hostname:
2077 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2078
skobes6468b902016-10-24 08:45:10 -07002079 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2080 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2081 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002082 try:
skobes6468b902016-10-24 08:45:10 -07002083 scm_obj.apply_patch(patchset_object)
2084 except Exception as e:
2085 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002086 return 1
2087
2088 # If we had an issue, commit the current state and register the issue.
2089 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002090 self.SetIssue(self.GetIssue())
2091 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002092 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2093 'patch from issue %(i)s at patchset '
2094 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2095 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002096 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002097 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002098 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099 return 0
2100
2101 @staticmethod
2102 def ParseIssueURL(parsed_url):
2103 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2104 return None
wychen3c1c1722016-08-04 11:46:36 -07002105 # Rietveld patch: https://domain/<number>/#ps<patchset>
2106 match = re.match(r'/(\d+)/$', parsed_url.path)
2107 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2108 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002109 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002110 issue=int(match.group(1)),
2111 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002112 hostname=parsed_url.netloc,
2113 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 # Typical url: https://domain/<issue_number>[/[other]]
2115 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2116 if match:
skobes6468b902016-10-24 08:45:10 -07002117 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002119 hostname=parsed_url.netloc,
2120 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002121 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2122 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2123 if match:
skobes6468b902016-10-24 08:45:10 -07002124 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 issue=int(match.group(1)),
2126 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002127 hostname=parsed_url.netloc,
2128 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 return None
2130
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002131 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002132 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00002133 raise NotImplementedError
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002134
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002135
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002136class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002137 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002138 # auth_config is Rietveld thing, kept here to preserve interface only.
2139 super(_GerritChangelistImpl, self).__init__(changelist)
2140 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002141 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002142 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002143 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002144 # Map from change number (issue) to its detail cache.
2145 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002146
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002147 if codereview_host is not None:
2148 assert not codereview_host.startswith('https://'), codereview_host
2149 self._gerrit_host = codereview_host
2150 self._gerrit_server = 'https://%s' % codereview_host
2151
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002152 def _GetGerritHost(self):
2153 # Lazy load of configs.
2154 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002155 if self._gerrit_host and '.' not in self._gerrit_host:
2156 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2157 # This happens for internal stuff http://crbug.com/614312.
2158 parsed = urlparse.urlparse(self.GetRemoteUrl())
2159 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002160 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002161 ' Your current remote is: %s' % self.GetRemoteUrl())
2162 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2163 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002164 return self._gerrit_host
2165
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002166 def _GetGitHost(self):
2167 """Returns git host to be used when uploading change to Gerrit."""
2168 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2169
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170 def GetCodereviewServer(self):
2171 if not self._gerrit_server:
2172 # If we're on a branch then get the server potentially associated
2173 # with that branch.
2174 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002175 self._gerrit_server = self._GitGetBranchConfigValue(
2176 self.CodereviewServerConfigKey())
2177 if self._gerrit_server:
2178 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002179 if not self._gerrit_server:
2180 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2181 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002182 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002183 parts[0] = parts[0] + '-review'
2184 self._gerrit_host = '.'.join(parts)
2185 self._gerrit_server = 'https://%s' % self._gerrit_host
2186 return self._gerrit_server
2187
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002188 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002189 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002190 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002191 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002192 logging.warn('can\'t detect Gerrit project.')
2193 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002194 project = urlparse.urlparse(remote_url).path.strip('/')
2195 if project.endswith('.git'):
2196 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002197 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2198 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2199 # gitiles/git-over-https protocol. E.g.,
2200 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2201 # as
2202 # https://chromium.googlesource.com/v8/v8
2203 if project.startswith('a/'):
2204 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002205 return project
2206
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002207 def _GerritChangeIdentifier(self):
2208 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2209
2210 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002211 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002212 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002213 project = self._GetGerritProject()
2214 if project:
2215 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2216 # Fall back on still unique, but less efficient change number.
2217 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002218
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002219 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002220 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002221 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002222
tandrii5d48c322016-08-18 16:19:37 -07002223 @classmethod
2224 def PatchsetConfigKey(cls):
2225 return 'gerritpatchset'
2226
2227 @classmethod
2228 def CodereviewServerConfigKey(cls):
2229 return 'gerritserver'
2230
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002231 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002232 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002233 if settings.GetGerritSkipEnsureAuthenticated():
2234 # For projects with unusual authentication schemes.
2235 # See http://crbug.com/603378.
2236 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002237
2238 # Check presence of cookies only if using cookies-based auth method.
2239 cookie_auth = gerrit_util.Authenticator.get()
2240 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002241 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002242
2243 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002244 self.GetCodereviewServer()
2245 git_host = self._GetGitHost()
2246 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002247
2248 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2249 git_auth = cookie_auth.get_auth_header(git_host)
2250 if gerrit_auth and git_auth:
2251 if gerrit_auth == git_auth:
2252 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002253 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002254 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002255 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002256 ' %s\n'
2257 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002258 ' Consider running the following command:\n'
2259 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002260 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002261 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002262 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002263 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002264 cookie_auth.get_new_password_message(git_host)))
2265 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002266 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002267 return
2268 else:
2269 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002270 ([] if gerrit_auth else [self._gerrit_host]) +
2271 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002272 DieWithError('Credentials for the following hosts are required:\n'
2273 ' %s\n'
2274 'These are read from %s (or legacy %s)\n'
2275 '%s' % (
2276 '\n '.join(missing),
2277 cookie_auth.get_gitcookies_path(),
2278 cookie_auth.get_netrc_path(),
2279 cookie_auth.get_new_password_message(git_host)))
2280
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002281 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002282 if not self.GetIssue():
2283 return
2284
2285 # Warm change details cache now to avoid RPCs later, reducing latency for
2286 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002287 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002288 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002289
2290 status = self._GetChangeDetail()['status']
2291 if status in ('MERGED', 'ABANDONED'):
2292 DieWithError('Change %s has been %s, new uploads are not allowed' %
2293 (self.GetIssueURL(),
2294 'submitted' if status == 'MERGED' else 'abandoned'))
2295
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002296 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2297 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2298 # Apparently this check is not very important? Otherwise get_auth_email
2299 # could have been added to other implementations of Authenticator.
2300 cookies_auth = gerrit_util.Authenticator.get()
2301 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002302 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002303
2304 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002305 if self.GetIssueOwner() == cookies_user:
2306 return
2307 logging.debug('change %s owner is %s, cookies user is %s',
2308 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002309 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002310 # so ask what Gerrit thinks of this user.
2311 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2312 if details['email'] == self.GetIssueOwner():
2313 return
2314 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002315 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002316 'as %s.\n'
2317 'Uploading may fail due to lack of permissions.' %
2318 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2319 confirm_or_exit(action='upload')
2320
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002321 def _PostUnsetIssueProperties(self):
2322 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002323 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002324
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002325 def GetGerritObjForPresubmit(self):
2326 return presubmit_support.GerritAccessor(self._GetGerritHost())
2327
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002328 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002329 """Apply a rough heuristic to give a simple summary of an issue's review
2330 or CQ status, assuming adherence to a common workflow.
2331
2332 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002333 * 'error' - error from review tool (including deleted issues)
2334 * 'unsent' - no reviewers added
2335 * 'waiting' - waiting for review
2336 * 'reply' - waiting for uploader to reply to review
2337 * 'lgtm' - Code-Review label has been set
2338 * 'commit' - in the commit queue
2339 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002340 """
2341 if not self.GetIssue():
2342 return None
2343
2344 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002345 data = self._GetChangeDetail([
2346 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002347 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002348 return 'error'
2349
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002350 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002351 return 'closed'
2352
Aaron Gable9ab38c62017-04-06 14:36:33 -07002353 if data['labels'].get('Commit-Queue', {}).get('approved'):
2354 # The section will have an "approved" subsection if anyone has voted
2355 # the maximum value on the label.
2356 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002357
Aaron Gable9ab38c62017-04-06 14:36:33 -07002358 if data['labels'].get('Code-Review', {}).get('approved'):
2359 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002360
2361 if not data.get('reviewers', {}).get('REVIEWER', []):
2362 return 'unsent'
2363
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002364 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002365 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2366 last_message_author = messages.pop().get('author', {})
2367 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002368 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2369 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002370 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002371 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002372 if last_message_author.get('_account_id') == owner:
2373 # Most recent message was by owner.
2374 return 'waiting'
2375 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002376 # Some reply from non-owner.
2377 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002378
2379 # Somehow there are no messages even though there are reviewers.
2380 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381
2382 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002383 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002384 patchset = data['revisions'][data['current_revision']]['_number']
2385 self.SetPatchset(patchset)
2386 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002387
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002388 def FetchDescription(self, force=False):
2389 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2390 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002391 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002392 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002393
dsansomee2d6fd92016-09-08 00:10:47 -07002394 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002395 if gerrit_util.HasPendingChangeEdit(
2396 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002397 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002398 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002399 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002400 'unpublished edit. Either publish the edit in the Gerrit web UI '
2401 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002402
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002403 gerrit_util.DeletePendingChangeEdit(
2404 self._GetGerritHost(), self._GerritChangeIdentifier())
2405 gerrit_util.SetCommitMessage(
2406 self._GetGerritHost(), self._GerritChangeIdentifier(),
2407 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002408
Aaron Gable636b13f2017-07-14 10:42:48 -07002409 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002410 gerrit_util.SetReview(
2411 self._GetGerritHost(), self._GerritChangeIdentifier(),
2412 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002413
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002414 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002415 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002416 messages = self._GetChangeDetail(
2417 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2418 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002419 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002420
2421 # Build dictionary of file comments for easy access and sorting later.
2422 # {author+date: {path: {patchset: {line: url+message}}}}
2423 comments = collections.defaultdict(
2424 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2425 for path, line_comments in file_comments.iteritems():
2426 for comment in line_comments:
2427 if comment.get('tag', '').startswith('autogenerated'):
2428 continue
2429 key = (comment['author']['email'], comment['updated'])
2430 if comment.get('side', 'REVISION') == 'PARENT':
2431 patchset = 'Base'
2432 else:
2433 patchset = 'PS%d' % comment['patch_set']
2434 line = comment.get('line', 0)
2435 url = ('https://%s/c/%s/%s/%s#%s%s' %
2436 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2437 'b' if comment.get('side') == 'PARENT' else '',
2438 str(line) if line else ''))
2439 comments[key][path][patchset][line] = (url, comment['message'])
2440
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002441 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002442 for msg in messages:
2443 # Don't bother showing autogenerated messages.
2444 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2445 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002446 # Gerrit spits out nanoseconds.
2447 assert len(msg['date'].split('.')[-1]) == 9
2448 date = datetime.datetime.strptime(msg['date'][:-3],
2449 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002450 message = msg['message']
2451 key = (msg['author']['email'], msg['date'])
2452 if key in comments:
2453 message += '\n'
2454 for path, patchsets in sorted(comments.get(key, {}).items()):
2455 if readable:
2456 message += '\n%s' % path
2457 for patchset, lines in sorted(patchsets.items()):
2458 for line, (url, content) in sorted(lines.items()):
2459 if line:
2460 line_str = 'Line %d' % line
2461 path_str = '%s:%d:' % (path, line)
2462 else:
2463 line_str = 'File comment'
2464 path_str = '%s:0:' % path
2465 if readable:
2466 message += '\n %s, %s: %s' % (patchset, line_str, url)
2467 message += '\n %s\n' % content
2468 else:
2469 message += '\n%s ' % path_str
2470 message += '\n%s\n' % content
2471
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002472 summary.append(_CommentSummary(
2473 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002474 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002475 sender=msg['author']['email'],
2476 # These could be inferred from the text messages and correlated with
2477 # Code-Review label maximum, however this is not reliable.
2478 # Leaving as is until the need arises.
2479 approval=False,
2480 disapproval=False,
2481 ))
2482 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002483
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002484 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002485 gerrit_util.AbandonChange(
2486 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002487
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002488 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002489 gerrit_util.SubmitChange(
2490 self._GetGerritHost(), self._GerritChangeIdentifier(),
2491 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002492
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002493 def _GetChangeDetail(self, options=None, no_cache=False):
2494 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002495
2496 If fresh data is needed, set no_cache=True which will clear cache and
2497 thus new data will be fetched from Gerrit.
2498 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002500 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002501
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002502 # Optimization to avoid multiple RPCs:
2503 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2504 'CURRENT_COMMIT' not in options):
2505 options.append('CURRENT_COMMIT')
2506
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002507 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002508 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002509 options = [o.upper() for o in options]
2510
2511 # Check in cache first unless no_cache is True.
2512 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002513 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002514 else:
2515 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002516 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002517 # Assumption: data fetched before with extra options is suitable
2518 # for return for a smaller set of options.
2519 # For example, if we cached data for
2520 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2521 # and request is for options=[CURRENT_REVISION],
2522 # THEN we can return prior cached data.
2523 if options_set.issubset(cached_options_set):
2524 return data
2525
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002526 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002527 data = gerrit_util.GetChangeDetail(
2528 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002529 except gerrit_util.GerritError as e:
2530 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002531 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002532 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002533
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002534 self._detail_cache.setdefault(cache_key, []).append(
2535 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002536 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002537
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002538 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002539 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002540 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002541 data = gerrit_util.GetChangeCommit(
2542 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002543 except gerrit_util.GerritError as e:
2544 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002545 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002546 raise
agable32978d92016-11-01 12:55:02 -07002547 return data
2548
Olivier Robin75ee7252018-04-13 10:02:56 +02002549 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002550 if git_common.is_dirty_git_tree('land'):
2551 return 1
tandriid60367b2016-06-22 05:25:12 -07002552 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2553 if u'Commit-Queue' in detail.get('labels', {}):
2554 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002555 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2556 'which can test and land changes for you. '
2557 'Are you sure you wish to bypass it?\n',
2558 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002559
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002560 differs = True
tandriic4344b52016-08-29 06:04:54 -07002561 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002562 # Note: git diff outputs nothing if there is no diff.
2563 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002564 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002565 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002566 if detail['current_revision'] == last_upload:
2567 differs = False
2568 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002569 print('WARNING: Local branch contents differ from latest uploaded '
2570 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002571 if differs:
2572 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002573 confirm_or_exit(
2574 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2575 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002576 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002577 elif not bypass_hooks:
2578 hook_results = self.RunHook(
2579 committing=True,
2580 may_prompt=not force,
2581 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002582 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2583 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002584 if not hook_results.should_continue():
2585 return 1
2586
2587 self.SubmitIssue(wait_for_merge=True)
2588 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002589 links = self._GetChangeCommit().get('web_links', [])
2590 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002591 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002592 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002593 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002594 return 0
2595
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002596 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002597 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002598 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002599 assert not directory
2600 assert parsed_issue_arg.valid
2601
2602 self._changelist.issue = parsed_issue_arg.issue
2603
2604 if parsed_issue_arg.hostname:
2605 self._gerrit_host = parsed_issue_arg.hostname
2606 self._gerrit_server = 'https://%s' % self._gerrit_host
2607
tandriic2405f52016-10-10 08:13:15 -07002608 try:
2609 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002610 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002611 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002612
2613 if not parsed_issue_arg.patchset:
2614 # Use current revision by default.
2615 revision_info = detail['revisions'][detail['current_revision']]
2616 patchset = int(revision_info['_number'])
2617 else:
2618 patchset = parsed_issue_arg.patchset
2619 for revision_info in detail['revisions'].itervalues():
2620 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2621 break
2622 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002623 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002624 (parsed_issue_arg.patchset, self.GetIssue()))
2625
Aaron Gable697a91b2018-01-19 15:20:15 -08002626 remote_url = self._changelist.GetRemoteUrl()
2627 if remote_url.endswith('.git'):
2628 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002629 remote_url = remote_url.rstrip('/')
2630
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002631 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002632 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002633
2634 if remote_url != fetch_info['url']:
2635 DieWithError('Trying to patch a change from %s but this repo appears '
2636 'to be %s.' % (fetch_info['url'], remote_url))
2637
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002638 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002639
Aaron Gable62619a32017-06-16 08:22:09 -07002640 if force:
2641 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2642 print('Checked out commit for change %i patchset %i locally' %
2643 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002644 elif nocommit:
2645 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2646 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002647 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002648 RunGit(['cherry-pick', 'FETCH_HEAD'])
2649 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002650 (parsed_issue_arg.issue, patchset))
2651 print('Note: this created a local commit which does not have '
2652 'the same hash as the one uploaded for review. This will make '
2653 'uploading changes based on top of this branch difficult.\n'
2654 'If you want to do that, use "git cl patch --force" instead.')
2655
Stefan Zagerd08043c2017-10-12 12:07:02 -07002656 if self.GetBranch():
2657 self.SetIssue(parsed_issue_arg.issue)
2658 self.SetPatchset(patchset)
2659 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2660 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2661 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2662 else:
2663 print('WARNING: You are in detached HEAD state.\n'
2664 'The patch has been applied to your checkout, but you will not be '
2665 'able to upload a new patch set to the gerrit issue.\n'
2666 'Try using the \'-b\' option if you would like to work on a '
2667 'branch and/or upload a new patch set.')
2668
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002669 return 0
2670
2671 @staticmethod
2672 def ParseIssueURL(parsed_url):
2673 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2674 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002675 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2676 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002677 # Short urls like https://domain/<issue_number> can be used, but don't allow
2678 # specifying the patchset (you'd 404), but we allow that here.
2679 if parsed_url.path == '/':
2680 part = parsed_url.fragment
2681 else:
2682 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002683 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002684 if match:
2685 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002686 issue=int(match.group(3)),
2687 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002688 hostname=parsed_url.netloc,
2689 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002690 return None
2691
tandrii16e0b4e2016-06-07 10:34:28 -07002692 def _GerritCommitMsgHookCheck(self, offer_removal):
2693 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2694 if not os.path.exists(hook):
2695 return
2696 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2697 # custom developer made one.
2698 data = gclient_utils.FileRead(hook)
2699 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2700 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002701 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002702 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002703 'and may interfere with it in subtle ways.\n'
2704 'We recommend you remove the commit-msg hook.')
2705 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002706 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002707 gclient_utils.rm_file_or_tree(hook)
2708 print('Gerrit commit-msg hook removed.')
2709 else:
2710 print('OK, will keep Gerrit commit-msg hook in place.')
2711
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002712 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002713 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002714 if options.squash and options.no_squash:
2715 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002716
2717 if not options.squash and not options.no_squash:
2718 # Load default for user, repo, squash=true, in this order.
2719 options.squash = settings.GetSquashGerritUploads()
2720 elif options.no_squash:
2721 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002722
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002723 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002724 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002725
Aaron Gableb56ad332017-01-06 15:24:31 -08002726 # This may be None; default fallback value is determined in logic below.
2727 title = options.title
2728
Dominic Battre7d1c4842017-10-27 09:17:28 +02002729 # Extract bug number from branch name.
2730 bug = options.bug
2731 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2732 if not bug and match:
2733 bug = match.group(1)
2734
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002736 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002737 if self.GetIssue():
2738 # Try to get the message from a previous upload.
2739 message = self.GetDescription()
2740 if not message:
2741 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002742 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002744 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002745 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002746 # When uploading a subsequent patchset, -m|--message is taken
2747 # as the patchset title if --title was not provided.
2748 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002749 else:
2750 default_title = RunGit(
2751 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002752 if options.force:
2753 title = default_title
2754 else:
2755 title = ask_for_data(
2756 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 change_id = self._GetChangeDetail()['change_id']
2758 while True:
2759 footer_change_ids = git_footers.get_footer_change_id(message)
2760 if footer_change_ids == [change_id]:
2761 break
2762 if not footer_change_ids:
2763 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002764 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002765 continue
2766 # There is already a valid footer but with different or several ids.
2767 # Doing this automatically is non-trivial as we don't want to lose
2768 # existing other footers, yet we want to append just 1 desired
2769 # Change-Id. Thus, just create a new footer, but let user verify the
2770 # new description.
2771 message = '%s\n\nChange-Id: %s' % (message, change_id)
2772 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002773 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002774 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002775 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 'Please, check the proposed correction to the description, '
2777 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2778 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2779 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002780 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002781 if not options.force:
2782 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002783 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002784 message = change_desc.description
2785 if not message:
2786 DieWithError("Description is empty. Aborting...")
2787 # Continue the while loop.
2788 # Sanity check of this code - we should end up with proper message
2789 # footer.
2790 assert [change_id] == git_footers.get_footer_change_id(message)
2791 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002792 else: # if not self.GetIssue()
2793 if options.message:
2794 message = options.message
2795 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002796 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002797 if options.title:
2798 message = options.title + '\n\n' + message
2799 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002800
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002801 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002802 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002803 # On first upload, patchset title is always this string, while
2804 # --title flag gets converted to first line of message.
2805 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002806 if not change_desc.description:
2807 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002808 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002809 if len(change_ids) > 1:
2810 DieWithError('too many Change-Id footers, at most 1 allowed.')
2811 if not change_ids:
2812 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002813 change_desc.set_description(git_footers.add_footer_change_id(
2814 change_desc.description,
2815 GenerateGerritChangeId(change_desc.description)))
2816 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002817 assert len(change_ids) == 1
2818 change_id = change_ids[0]
2819
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002820 if options.reviewers or options.tbrs or options.add_owners_to:
2821 change_desc.update_reviewers(options.reviewers, options.tbrs,
2822 options.add_owners_to, change)
2823
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002824 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002825 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2826 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002827 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002828 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2829 desc_tempfile.write(change_desc.description)
2830 desc_tempfile.close()
2831 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2832 '-F', desc_tempfile.name]).strip()
2833 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002834 else:
2835 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002836 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002837 if not change_desc.description:
2838 DieWithError("Description is empty. Aborting...")
2839
2840 if not git_footers.get_footer_change_id(change_desc.description):
2841 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002842 change_desc.set_description(
2843 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002844 if options.reviewers or options.tbrs or options.add_owners_to:
2845 change_desc.update_reviewers(options.reviewers, options.tbrs,
2846 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002847 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002848 # For no-squash mode, we assume the remote called "origin" is the one we
2849 # want. It is not worthwhile to support different workflows for
2850 # no-squash mode.
2851 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2853
2854 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002855 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002856 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2857 ref_to_push)]).splitlines()
2858 if len(commits) > 1:
2859 print('WARNING: This will upload %d commits. Run the following command '
2860 'to see which commits will be uploaded: ' % len(commits))
2861 print('git log %s..%s' % (parent, ref_to_push))
2862 print('You can also use `git squash-branch` to squash these into a '
2863 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002864 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002865
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002866 if options.reviewers or options.tbrs or options.add_owners_to:
2867 change_desc.update_reviewers(options.reviewers, options.tbrs,
2868 options.add_owners_to, change)
2869
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002870 reviewers = sorted(change_desc.get_reviewers())
2871 # Add cc's from the CC_LIST and --cc flag (if any).
2872 if not options.private and not options.no_autocc:
2873 cc = self.GetCCList().split(',')
2874 else:
2875 cc = []
2876 if options.cc:
2877 cc.extend(options.cc)
2878 cc = filter(None, [email.strip() for email in cc])
2879 if change_desc.get_cced():
2880 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002881 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2882 valid_accounts = set(reviewers + cc)
2883 # TODO(crbug/877717): relax this for all hosts.
2884 else:
2885 valid_accounts = gerrit_util.ValidAccounts(
2886 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002887 logging.info('accounts %s are recognized, %s invalid',
2888 sorted(valid_accounts),
2889 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002890
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002891 # Extra options that can be specified at push time. Doc:
2892 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002893 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002894
Aaron Gable844cf292017-06-28 11:32:59 -07002895 # By default, new changes are started in WIP mode, and subsequent patchsets
2896 # don't send email. At any time, passing --send-mail will mark the change
2897 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002898 if options.send_mail:
2899 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002900 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002901 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002902 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002903 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002904 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002905
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002906 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002907 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002908
Aaron Gable9b713dd2016-12-14 16:04:21 -08002909 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002910 # Punctuation and whitespace in |title| must be percent-encoded.
2911 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002912
agablec6787972016-09-09 16:13:34 -07002913 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002914 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002915
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002916 for r in sorted(reviewers):
2917 if r in valid_accounts:
2918 refspec_opts.append('r=%s' % r)
2919 reviewers.remove(r)
2920 else:
2921 # TODO(tandrii): this should probably be a hard failure.
2922 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2923 % r)
2924 for c in sorted(cc):
2925 # refspec option will be rejected if cc doesn't correspond to an
2926 # account, even though REST call to add such arbitrary cc may succeed.
2927 if c in valid_accounts:
2928 refspec_opts.append('cc=%s' % c)
2929 cc.remove(c)
2930
rmistry9eadede2016-09-19 11:22:43 -07002931 if options.topic:
2932 # Documentation on Gerrit topics is here:
2933 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002934 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002935
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002936 if not change_desc.get_reviewers(tbr_only=True):
2937 # Change is not TBR, so we can inline setting other labels, too.
2938 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2939 # max score for CR label somehow.
2940 if options.enable_auto_submit:
2941 refspec_opts.append('l=Auto-Submit+1')
2942 if options.use_commit_queue:
2943 refspec_opts.append('l=Commit-Queue+2')
2944 elif options.cq_dry_run:
2945 refspec_opts.append('l=Commit-Queue+1')
2946
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002947 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002948 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002949 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002950 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002951 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2952
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002953 refspec_suffix = ''
2954 if refspec_opts:
2955 refspec_suffix = '%' + ','.join(refspec_opts)
2956 assert ' ' not in refspec_suffix, (
2957 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2958 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2959
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002960 try:
Edward Lemur83bd7f42018-10-10 00:14:21 +00002961 # TODO(crbug.com/881860): Remove.
Edward Lemur47faa062018-10-11 19:46:02 +00002962 # Clear the log after each git-cl upload run by setting mode='w'.
2963 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
2964 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
2965
2966 GERRIT_ERR_LOGGER.addHandler(handler)
2967 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
2968 # Don't propagate to root logger, so that logs are not printed.
2969 GERRIT_ERR_LOGGER.propagate = 0
2970
Edward Lemur83bd7f42018-10-10 00:14:21 +00002971 # Get interesting headers from git push, to be displayed to the user if
2972 # subsequent Gerrit RPC calls fail.
2973 env = os.environ.copy()
2974 env['GIT_CURL_VERBOSE'] = '1'
2975 class FilterHeaders(object):
2976 """Filter git push headers and store them in a file.
2977
2978 Regular git push output is printed directly.
2979 """
2980
2981 def __init__(self):
2982 # The output from git push that we want to store in a file.
2983 self._output = ''
2984 # Keeps track of whether the current line is part of a request header.
2985 self._on_header = False
2986 # Keeps track of repeated empty lines, which mark the end of a request
2987 # header.
2988 self._last_line_empty = False
2989
2990 def __call__(self, line):
2991 """Handle a single line of git push output."""
2992 if not line:
2993 # Two consecutive empty lines mark the end of a header.
2994 if self._last_line_empty:
2995 self._on_header = False
2996 self._last_line_empty = True
2997 return
2998
2999 self._last_line_empty = False
3000 # A line starting with '>' marks the beggining of a request header.
3001 if line[0] == '>':
3002 self._on_header = True
3003 GERRIT_ERR_LOGGER.info(line)
3004 # Lines not starting with '*' or '<', and not part of a request header
3005 # should be displayed to the user.
3006 elif line[0] not in '*<' and not self._on_header:
3007 print(line)
3008 # Flush after every line: useful for seeing progress when running as
3009 # recipe.
3010 sys.stdout.flush()
3011 # Filter out the cookie and authorization headers.
3012 elif ('cookie: ' not in line.lower()
3013 and 'authorization: ' not in line.lower()):
3014 GERRIT_ERR_LOGGER.info(line)
3015
3016 filter_fn = FilterHeaders()
Edward Lemur01f4a4f2018-11-03 00:40:38 +00003017 before_push = time_time()
Edward Lemurfec80c42018-11-01 23:14:14 +00003018 push_returncode = 0
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003019 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003020 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00003021 print_stdout=False,
3022 filter_fn=filter_fn,
3023 env=env)
Edward Lemurfec80c42018-11-01 23:14:14 +00003024 except subprocess2.CalledProcessError as e:
3025 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003026 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003027 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003028 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003029 'credential problems:\n'
3030 ' git cl creds-check\n',
3031 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00003032 finally:
3033 metrics.collector.add_repeated('sub_commands', {
3034 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00003035 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00003036 'exit_code': push_returncode,
3037 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
3038 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003039
3040 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003041 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003042 change_numbers = [m.group(1)
3043 for m in map(regex.match, push_stdout.splitlines())
3044 if m]
3045 if len(change_numbers) != 1:
3046 DieWithError(
3047 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003048 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003049 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003050 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003051
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00003052 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003053 # GetIssue() is not set in case of non-squash uploads according to tests.
3054 # TODO(agable): non-squash uploads in git cl should be removed.
3055 gerrit_util.AddReviewers(
3056 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003057 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003058 reviewers, cc,
3059 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003060
Aaron Gablefd238082017-06-07 13:42:34 -07003061 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003062 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3063 score = 1
3064 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3065 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3066 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003067 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003068 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003069 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003070 msg='Self-approving for TBR',
3071 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00003072 # Labels aren't set through refspec only if tbr is set (see check above).
3073 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3074 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003075 return 0
3076
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003077 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3078 change_desc):
3079 """Computes parent of the generated commit to be uploaded to Gerrit.
3080
3081 Returns revision or a ref name.
3082 """
3083 if custom_cl_base:
3084 # Try to avoid creating additional unintended CLs when uploading, unless
3085 # user wants to take this risk.
3086 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3087 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3088 local_ref_of_target_remote])
3089 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003090 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003091 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3092 'If you proceed with upload, more than 1 CL may be created by '
3093 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3094 'If you are certain that specified base `%s` has already been '
3095 'uploaded to Gerrit as another CL, you may proceed.\n' %
3096 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3097 if not force:
3098 confirm_or_exit(
3099 'Do you take responsibility for cleaning up potential mess '
3100 'resulting from proceeding with upload?',
3101 action='upload')
3102 return custom_cl_base
3103
Aaron Gablef97e33d2017-03-30 15:44:27 -07003104 if remote != '.':
3105 return self.GetCommonAncestorWithUpstream()
3106
3107 # If our upstream branch is local, we base our squashed commit on its
3108 # squashed version.
3109 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3110
Aaron Gablef97e33d2017-03-30 15:44:27 -07003111 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003112 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003113
3114 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003115 # TODO(tandrii): consider checking parent change in Gerrit and using its
3116 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3117 # the tree hash of the parent branch. The upside is less likely bogus
3118 # requests to reupload parent change just because it's uploadhash is
3119 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003120 parent = RunGit(['config',
3121 'branch.%s.gerritsquashhash' % upstream_branch_name],
3122 error_ok=True).strip()
3123 # Verify that the upstream branch has been uploaded too, otherwise
3124 # Gerrit will create additional CLs when uploading.
3125 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3126 RunGitSilent(['rev-parse', parent + ':'])):
3127 DieWithError(
3128 '\nUpload upstream branch %s first.\n'
3129 'It is likely that this branch has been rebased since its last '
3130 'upload, so you just need to upload it again.\n'
3131 '(If you uploaded it with --no-squash, then branch dependencies '
3132 'are not supported, and you should reupload with --squash.)'
3133 % upstream_branch_name,
3134 change_desc)
3135 return parent
3136
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003137 def _AddChangeIdToCommitMessage(self, options, args):
3138 """Re-commits using the current message, assumes the commit hook is in
3139 place.
3140 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003141 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003142 git_command = ['commit', '--amend', '-m', log_desc]
3143 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003144 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003145 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003146 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003147 return new_log_desc
3148 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003149 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003150
Ravi Mistry31e7d562018-04-02 12:53:57 -04003151 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3152 """Sets labels on the change based on the provided flags."""
3153 labels = {}
3154 notify = None;
3155 if enable_auto_submit:
3156 labels['Auto-Submit'] = 1
3157 if use_commit_queue:
3158 labels['Commit-Queue'] = 2
3159 elif cq_dry_run:
3160 labels['Commit-Queue'] = 1
3161 notify = False
3162 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003163 gerrit_util.SetReview(
3164 self._GetGerritHost(),
3165 self._GerritChangeIdentifier(),
3166 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003167
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003168 def SetCQState(self, new_state):
3169 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003170 vote_map = {
3171 _CQState.NONE: 0,
3172 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003173 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003174 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003175 labels = {'Commit-Queue': vote_map[new_state]}
3176 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003177 gerrit_util.SetReview(
3178 self._GetGerritHost(), self._GerritChangeIdentifier(),
3179 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003180
tandriie113dfd2016-10-11 10:20:12 -07003181 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003182 try:
3183 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003184 except GerritChangeNotExists:
3185 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003186
3187 if data['status'] in ('ABANDONED', 'MERGED'):
3188 return 'CL %s is closed' % self.GetIssue()
3189
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003190 def GetTryJobProperties(self, patchset=None):
3191 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003192 data = self._GetChangeDetail(['ALL_REVISIONS'])
3193 patchset = int(patchset or self.GetPatchset())
3194 assert patchset
3195 revision_data = None # Pylint wants it to be defined.
3196 for revision_data in data['revisions'].itervalues():
3197 if int(revision_data['_number']) == patchset:
3198 break
3199 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003200 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003201 (patchset, self.GetIssue()))
3202 return {
3203 'patch_issue': self.GetIssue(),
3204 'patch_set': patchset or self.GetPatchset(),
3205 'patch_project': data['project'],
3206 'patch_storage': 'gerrit',
3207 'patch_ref': revision_data['fetch']['http']['ref'],
3208 'patch_repository_url': revision_data['fetch']['http']['url'],
3209 'patch_gerrit_url': self.GetCodereviewServer(),
3210 }
tandriie113dfd2016-10-11 10:20:12 -07003211
tandriide281ae2016-10-12 06:02:30 -07003212 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003213 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003214
Edward Lemur707d70b2018-02-07 00:50:14 +01003215 def GetReviewers(self):
3216 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3217 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3218
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003219
3220_CODEREVIEW_IMPLEMENTATIONS = {
3221 'rietveld': _RietveldChangelistImpl,
3222 'gerrit': _GerritChangelistImpl,
3223}
3224
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003225
iannuccie53c9352016-08-17 14:40:40 -07003226def _add_codereview_issue_select_options(parser, extra=""):
3227 _add_codereview_select_options(parser)
3228
3229 text = ('Operate on this issue number instead of the current branch\'s '
3230 'implicit issue.')
3231 if extra:
3232 text += ' '+extra
3233 parser.add_option('-i', '--issue', type=int, help=text)
3234
3235
3236def _process_codereview_issue_select_options(parser, options):
3237 _process_codereview_select_options(parser, options)
3238 if options.issue is not None and not options.forced_codereview:
3239 parser.error('--issue must be specified with either --rietveld or --gerrit')
3240
3241
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003242def _add_codereview_select_options(parser):
3243 """Appends --gerrit and --rietveld options to force specific codereview."""
3244 parser.codereview_group = optparse.OptionGroup(
3245 parser, 'EXPERIMENTAL! Codereview override options')
3246 parser.add_option_group(parser.codereview_group)
3247 parser.codereview_group.add_option(
3248 '--gerrit', action='store_true',
3249 help='Force the use of Gerrit for codereview')
3250 parser.codereview_group.add_option(
3251 '--rietveld', action='store_true',
3252 help='Force the use of Rietveld for codereview')
3253
3254
3255def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003256 if options.rietveld:
3257 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003258 options.forced_codereview = None
3259 if options.gerrit:
3260 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003261
3262
tandriif9aefb72016-07-01 09:06:51 -07003263def _get_bug_line_values(default_project, bugs):
3264 """Given default_project and comma separated list of bugs, yields bug line
3265 values.
3266
3267 Each bug can be either:
3268 * a number, which is combined with default_project
3269 * string, which is left as is.
3270
3271 This function may produce more than one line, because bugdroid expects one
3272 project per line.
3273
3274 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3275 ['v8:123', 'chromium:789']
3276 """
3277 default_bugs = []
3278 others = []
3279 for bug in bugs.split(','):
3280 bug = bug.strip()
3281 if bug:
3282 try:
3283 default_bugs.append(int(bug))
3284 except ValueError:
3285 others.append(bug)
3286
3287 if default_bugs:
3288 default_bugs = ','.join(map(str, default_bugs))
3289 if default_project:
3290 yield '%s:%s' % (default_project, default_bugs)
3291 else:
3292 yield default_bugs
3293 for other in sorted(others):
3294 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3295 yield other
3296
3297
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003298class ChangeDescription(object):
3299 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003300 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003301 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003302 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003303 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003304 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3305 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3306 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3307 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003308
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003309 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003310 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003311
agable@chromium.org42c20792013-09-12 17:34:49 +00003312 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003313 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003314 return '\n'.join(self._description_lines)
3315
3316 def set_description(self, desc):
3317 if isinstance(desc, basestring):
3318 lines = desc.splitlines()
3319 else:
3320 lines = [line.rstrip() for line in desc]
3321 while lines and not lines[0]:
3322 lines.pop(0)
3323 while lines and not lines[-1]:
3324 lines.pop(-1)
3325 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003326
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003327 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3328 """Rewrites the R=/TBR= line(s) as a single line each.
3329
3330 Args:
3331 reviewers (list(str)) - list of additional emails to use for reviewers.
3332 tbrs (list(str)) - list of additional emails to use for TBRs.
3333 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3334 the change that are missing OWNER coverage. If this is not None, you
3335 must also pass a value for `change`.
3336 change (Change) - The Change that should be used for OWNERS lookups.
3337 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003338 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003339 assert isinstance(tbrs, list), tbrs
3340
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003341 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003342 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003343
3344 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003345 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003346
3347 reviewers = set(reviewers)
3348 tbrs = set(tbrs)
3349 LOOKUP = {
3350 'TBR': tbrs,
3351 'R': reviewers,
3352 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003353
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003354 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003355 regexp = re.compile(self.R_LINE)
3356 matches = [regexp.match(line) for line in self._description_lines]
3357 new_desc = [l for i, l in enumerate(self._description_lines)
3358 if not matches[i]]
3359 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003360
agable@chromium.org42c20792013-09-12 17:34:49 +00003361 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003362
3363 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003364 for match in matches:
3365 if not match:
3366 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003367 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3368
3369 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003370 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003371 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003372 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003373 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003374 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003375 LOOKUP[add_owners_to].update(
3376 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003377
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003378 # If any folks ended up in both groups, remove them from tbrs.
3379 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003380
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003381 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3382 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003383
3384 # Put the new lines in the description where the old first R= line was.
3385 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3386 if 0 <= line_loc < len(self._description_lines):
3387 if new_tbr_line:
3388 self._description_lines.insert(line_loc, new_tbr_line)
3389 if new_r_line:
3390 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003391 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003392 if new_r_line:
3393 self.append_footer(new_r_line)
3394 if new_tbr_line:
3395 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003396
Aaron Gable3a16ed12017-03-23 10:51:55 -07003397 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003398 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003399 self.set_description([
3400 '# Enter a description of the change.',
3401 '# This will be displayed on the codereview site.',
3402 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003403 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003404 '--------------------',
3405 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003406
agable@chromium.org42c20792013-09-12 17:34:49 +00003407 regexp = re.compile(self.BUG_LINE)
3408 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003409 prefix = settings.GetBugPrefix()
3410 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003411 if git_footer:
3412 self.append_footer('Bug: %s' % ', '.join(values))
3413 else:
3414 for value in values:
3415 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003416
agable@chromium.org42c20792013-09-12 17:34:49 +00003417 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003418 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003419 if not content:
3420 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003421 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003422
Bruce Dawson2377b012018-01-11 16:46:49 -08003423 # Strip off comments and default inserted "Bug:" line.
3424 clean_lines = [line.rstrip() for line in lines if not
3425 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003426 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003427 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003428 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003429
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003430 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003431 """Adds a footer line to the description.
3432
3433 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3434 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3435 that Gerrit footers are always at the end.
3436 """
3437 parsed_footer_line = git_footers.parse_footer(line)
3438 if parsed_footer_line:
3439 # Line is a gerrit footer in the form: Footer-Key: any value.
3440 # Thus, must be appended observing Gerrit footer rules.
3441 self.set_description(
3442 git_footers.add_footer(self.description,
3443 key=parsed_footer_line[0],
3444 value=parsed_footer_line[1]))
3445 return
3446
3447 if not self._description_lines:
3448 self._description_lines.append(line)
3449 return
3450
3451 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3452 if gerrit_footers:
3453 # git_footers.split_footers ensures that there is an empty line before
3454 # actual (gerrit) footers, if any. We have to keep it that way.
3455 assert top_lines and top_lines[-1] == ''
3456 top_lines, separator = top_lines[:-1], top_lines[-1:]
3457 else:
3458 separator = [] # No need for separator if there are no gerrit_footers.
3459
3460 prev_line = top_lines[-1] if top_lines else ''
3461 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3462 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3463 top_lines.append('')
3464 top_lines.append(line)
3465 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003466
tandrii99a72f22016-08-17 14:33:24 -07003467 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003468 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003469 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003470 reviewers = [match.group(2).strip()
3471 for match in matches
3472 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003473 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003474
bradnelsond975b302016-10-23 12:20:23 -07003475 def get_cced(self):
3476 """Retrieves the list of reviewers."""
3477 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3478 cced = [match.group(2).strip() for match in matches if match]
3479 return cleanup_list(cced)
3480
Nodir Turakulov23b82142017-11-16 11:04:25 -08003481 def get_hash_tags(self):
3482 """Extracts and sanitizes a list of Gerrit hashtags."""
3483 subject = (self._description_lines or ('',))[0]
3484 subject = re.sub(
3485 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3486
3487 tags = []
3488 start = 0
3489 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3490 while True:
3491 m = bracket_exp.match(subject, start)
3492 if not m:
3493 break
3494 tags.append(self.sanitize_hash_tag(m.group(1)))
3495 start = m.end()
3496
3497 if not tags:
3498 # Try "Tag: " prefix.
3499 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3500 if m:
3501 tags.append(self.sanitize_hash_tag(m.group(1)))
3502 return tags
3503
3504 @classmethod
3505 def sanitize_hash_tag(cls, tag):
3506 """Returns a sanitized Gerrit hash tag.
3507
3508 A sanitized hashtag can be used as a git push refspec parameter value.
3509 """
3510 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3511
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003512 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3513 """Updates this commit description given the parent.
3514
3515 This is essentially what Gnumbd used to do.
3516 Consult https://goo.gl/WMmpDe for more details.
3517 """
3518 assert parent_msg # No, orphan branch creation isn't supported.
3519 assert parent_hash
3520 assert dest_ref
3521 parent_footer_map = git_footers.parse_footers(parent_msg)
3522 # This will also happily parse svn-position, which GnumbD is no longer
3523 # supporting. While we'd generate correct footers, the verifier plugin
3524 # installed in Gerrit will block such commit (ie git push below will fail).
3525 parent_position = git_footers.get_position(parent_footer_map)
3526
3527 # Cherry-picks may have last line obscuring their prior footers,
3528 # from git_footers perspective. This is also what Gnumbd did.
3529 cp_line = None
3530 if (self._description_lines and
3531 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3532 cp_line = self._description_lines.pop()
3533
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003534 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003535
3536 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3537 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003538 for i, line in enumerate(footer_lines):
3539 k, v = git_footers.parse_footer(line) or (None, None)
3540 if k and k.startswith('Cr-'):
3541 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003542
3543 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003544 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003545 if parent_position[0] == dest_ref:
3546 # Same branch as parent.
3547 number = int(parent_position[1]) + 1
3548 else:
3549 number = 1 # New branch, and extra lineage.
3550 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3551 int(parent_position[1])))
3552
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003553 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3554 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003555
3556 self._description_lines = top_lines
3557 if cp_line:
3558 self._description_lines.append(cp_line)
3559 if self._description_lines[-1] != '':
3560 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003561 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003562
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003563
Aaron Gablea1bab272017-04-11 16:38:18 -07003564def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003565 """Retrieves the reviewers that approved a CL from the issue properties with
3566 messages.
3567
3568 Note that the list may contain reviewers that are not committer, thus are not
3569 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003570
3571 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003572 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003573 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003574 return sorted(
3575 set(
3576 message['sender']
3577 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003578 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003579 )
3580 )
3581
3582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583def FindCodereviewSettingsFile(filename='codereview.settings'):
3584 """Finds the given file starting in the cwd and going up.
3585
3586 Only looks up to the top of the repository unless an
3587 'inherit-review-settings-ok' file exists in the root of the repository.
3588 """
3589 inherit_ok_file = 'inherit-review-settings-ok'
3590 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003591 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003592 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3593 root = '/'
3594 while True:
3595 if filename in os.listdir(cwd):
3596 if os.path.isfile(os.path.join(cwd, filename)):
3597 return open(os.path.join(cwd, filename))
3598 if cwd == root:
3599 break
3600 cwd = os.path.dirname(cwd)
3601
3602
3603def LoadCodereviewSettingsFromFile(fileobj):
3604 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003605 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003607 def SetProperty(name, setting, unset_error_ok=False):
3608 fullname = 'rietveld.' + name
3609 if setting in keyvals:
3610 RunGit(['config', fullname, keyvals[setting]])
3611 else:
3612 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3613
tandrii48df5812016-10-17 03:55:37 -07003614 if not keyvals.get('GERRIT_HOST', False):
3615 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 # Only server setting is required. Other settings can be absent.
3617 # In that case, we ignore errors raised during option deletion attempt.
3618 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003619 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3621 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003622 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003623 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3624 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003625 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003626 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3627 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003628
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003629 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003630 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003631
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003632 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003633 RunGit(['config', 'gerrit.squash-uploads',
3634 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003635
tandrii@chromium.org28253532016-04-14 13:46:56 +00003636 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003637 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003638 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003641 # should be of the form
3642 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3643 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3645 keyvals['ORIGIN_URL_CONFIG']])
3646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003648def urlretrieve(source, destination):
3649 """urllib is broken for SSL connections via a proxy therefore we
3650 can't use urllib.urlretrieve()."""
3651 with open(destination, 'w') as f:
3652 f.write(urllib2.urlopen(source).read())
3653
3654
ukai@chromium.org712d6102013-11-27 00:52:58 +00003655def hasSheBang(fname):
3656 """Checks fname is a #! script."""
3657 with open(fname) as f:
3658 return f.read(2).startswith('#!')
3659
3660
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003661# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3662def DownloadHooks(*args, **kwargs):
3663 pass
3664
3665
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003666def DownloadGerritHook(force):
3667 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003668
3669 Args:
3670 force: True to update hooks. False to install hooks if not present.
3671 """
3672 if not settings.GetIsGerrit():
3673 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003674 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003675 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3676 if not os.access(dst, os.X_OK):
3677 if os.path.exists(dst):
3678 if not force:
3679 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003680 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003681 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003682 if not hasSheBang(dst):
3683 DieWithError('Not a script: %s\n'
3684 'You need to download from\n%s\n'
3685 'into .git/hooks/commit-msg and '
3686 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003687 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3688 except Exception:
3689 if os.path.exists(dst):
3690 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003691 DieWithError('\nFailed to download hooks.\n'
3692 'You need to download from\n%s\n'
3693 'into .git/hooks/commit-msg and '
3694 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003695
3696
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003697def GetRietveldCodereviewSettingsInteractively():
3698 """Prompt the user for settings."""
3699 server = settings.GetDefaultServerUrl(error_ok=True)
3700 prompt = 'Rietveld server (host[:port])'
3701 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3702 newserver = ask_for_data(prompt + ':')
3703 if not server and not newserver:
3704 newserver = DEFAULT_SERVER
3705 if newserver:
3706 newserver = gclient_utils.UpgradeToHttps(newserver)
3707 if newserver != server:
3708 RunGit(['config', 'rietveld.server', newserver])
3709
3710 def SetProperty(initial, caption, name, is_url):
3711 prompt = caption
3712 if initial:
3713 prompt += ' ("x" to clear) [%s]' % initial
3714 new_val = ask_for_data(prompt + ':')
3715 if new_val == 'x':
3716 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3717 elif new_val:
3718 if is_url:
3719 new_val = gclient_utils.UpgradeToHttps(new_val)
3720 if new_val != initial:
3721 RunGit(['config', 'rietveld.' + name, new_val])
3722
3723 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3724 SetProperty(settings.GetDefaultPrivateFlag(),
3725 'Private flag (rietveld only)', 'private', False)
3726 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3727 'tree-status-url', False)
3728 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3729 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3730 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3731 'run-post-upload-hook', False)
3732
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003733
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003734class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003735 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003736
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003737 _GOOGLESOURCE = 'googlesource.com'
3738
3739 def __init__(self):
3740 # Cached list of [host, identity, source], where source is either
3741 # .gitcookies or .netrc.
3742 self._all_hosts = None
3743
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003744 def ensure_configured_gitcookies(self):
3745 """Runs checks and suggests fixes to make git use .gitcookies from default
3746 path."""
3747 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3748 configured_path = RunGitSilent(
3749 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003750 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003751 if configured_path:
3752 self._ensure_default_gitcookies_path(configured_path, default)
3753 else:
3754 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003755
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003756 @staticmethod
3757 def _ensure_default_gitcookies_path(configured_path, default_path):
3758 assert configured_path
3759 if configured_path == default_path:
3760 print('git is already configured to use your .gitcookies from %s' %
3761 configured_path)
3762 return
3763
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003764 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003765 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3766 (configured_path, default_path))
3767
3768 if not os.path.exists(configured_path):
3769 print('However, your configured .gitcookies file is missing.')
3770 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3771 action='reconfigure')
3772 RunGit(['config', '--global', 'http.cookiefile', default_path])
3773 return
3774
3775 if os.path.exists(default_path):
3776 print('WARNING: default .gitcookies file already exists %s' %
3777 default_path)
3778 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3779 default_path)
3780
3781 confirm_or_exit('Move existing .gitcookies to default location?',
3782 action='move')
3783 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003784 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003785 print('Moved and reconfigured git to use .gitcookies from %s' %
3786 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003787
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003788 @staticmethod
3789 def _configure_gitcookies_path(default_path):
3790 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3791 if os.path.exists(netrc_path):
3792 print('You seem to be using outdated .netrc for git credentials: %s' %
3793 netrc_path)
3794 print('This tool will guide you through setting up recommended '
3795 '.gitcookies store for git credentials.\n'
3796 '\n'
3797 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3798 ' git config --global --unset http.cookiefile\n'
3799 ' mv %s %s.backup\n\n' % (default_path, default_path))
3800 confirm_or_exit(action='setup .gitcookies')
3801 RunGit(['config', '--global', 'http.cookiefile', default_path])
3802 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003803
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003804 def get_hosts_with_creds(self, include_netrc=False):
3805 if self._all_hosts is None:
3806 a = gerrit_util.CookiesAuthenticator()
3807 self._all_hosts = [
3808 (h, u, s)
3809 for h, u, s in itertools.chain(
3810 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3811 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3812 )
3813 if h.endswith(self._GOOGLESOURCE)
3814 ]
3815
3816 if include_netrc:
3817 return self._all_hosts
3818 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3819
3820 def print_current_creds(self, include_netrc=False):
3821 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3822 if not hosts:
3823 print('No Git/Gerrit credentials found')
3824 return
3825 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3826 header = [('Host', 'User', 'Which file'),
3827 ['=' * l for l in lengths]]
3828 for row in (header + hosts):
3829 print('\t'.join((('%%+%ds' % l) % s)
3830 for l, s in zip(lengths, row)))
3831
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003832 @staticmethod
3833 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003834 """Parses identity "git-<username>.domain" into <username> and domain."""
3835 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003836 # distinguishable from sub-domains. But we do know typical domains:
3837 if identity.endswith('.chromium.org'):
3838 domain = 'chromium.org'
3839 username = identity[:-len('.chromium.org')]
3840 else:
3841 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003842 if username.startswith('git-'):
3843 username = username[len('git-'):]
3844 return username, domain
3845
3846 def _get_usernames_of_domain(self, domain):
3847 """Returns list of usernames referenced by .gitcookies in a given domain."""
3848 identities_by_domain = {}
3849 for _, identity, _ in self.get_hosts_with_creds():
3850 username, domain = self._parse_identity(identity)
3851 identities_by_domain.setdefault(domain, []).append(username)
3852 return identities_by_domain.get(domain)
3853
3854 def _canonical_git_googlesource_host(self, host):
3855 """Normalizes Gerrit hosts (with '-review') to Git host."""
3856 assert host.endswith(self._GOOGLESOURCE)
3857 # Prefix doesn't include '.' at the end.
3858 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3859 if prefix.endswith('-review'):
3860 prefix = prefix[:-len('-review')]
3861 return prefix + '.' + self._GOOGLESOURCE
3862
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003863 def _canonical_gerrit_googlesource_host(self, host):
3864 git_host = self._canonical_git_googlesource_host(host)
3865 prefix = git_host.split('.', 1)[0]
3866 return prefix + '-review.' + self._GOOGLESOURCE
3867
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003868 def _get_counterpart_host(self, host):
3869 assert host.endswith(self._GOOGLESOURCE)
3870 git = self._canonical_git_googlesource_host(host)
3871 gerrit = self._canonical_gerrit_googlesource_host(git)
3872 return git if gerrit == host else gerrit
3873
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003874 def has_generic_host(self):
3875 """Returns whether generic .googlesource.com has been configured.
3876
3877 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3878 """
3879 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3880 if host == '.' + self._GOOGLESOURCE:
3881 return True
3882 return False
3883
3884 def _get_git_gerrit_identity_pairs(self):
3885 """Returns map from canonic host to pair of identities (Git, Gerrit).
3886
3887 One of identities might be None, meaning not configured.
3888 """
3889 host_to_identity_pairs = {}
3890 for host, identity, _ in self.get_hosts_with_creds():
3891 canonical = self._canonical_git_googlesource_host(host)
3892 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3893 idx = 0 if canonical == host else 1
3894 pair[idx] = identity
3895 return host_to_identity_pairs
3896
3897 def get_partially_configured_hosts(self):
3898 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003899 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3900 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3901 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003902
3903 def get_conflicting_hosts(self):
3904 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003905 host
3906 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003907 if None not in (i1, i2) and i1 != i2)
3908
3909 def get_duplicated_hosts(self):
3910 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3911 return set(host for host, count in counters.iteritems() if count > 1)
3912
3913 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3914 'chromium.googlesource.com': 'chromium.org',
3915 'chrome-internal.googlesource.com': 'google.com',
3916 }
3917
3918 def get_hosts_with_wrong_identities(self):
3919 """Finds hosts which **likely** reference wrong identities.
3920
3921 Note: skips hosts which have conflicting identities for Git and Gerrit.
3922 """
3923 hosts = set()
3924 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3925 pair = self._get_git_gerrit_identity_pairs().get(host)
3926 if pair and pair[0] == pair[1]:
3927 _, domain = self._parse_identity(pair[0])
3928 if domain != expected:
3929 hosts.add(host)
3930 return hosts
3931
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003932 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003933 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003934 hosts = sorted(hosts)
3935 assert hosts
3936 if extra_column_func is None:
3937 extras = [''] * len(hosts)
3938 else:
3939 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003940 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3941 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003942 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003943 lines.append(tmpl % he)
3944 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003945
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003946 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003947 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003948 yield ('.googlesource.com wildcard record detected',
3949 ['Chrome Infrastructure team recommends to list full host names '
3950 'explicitly.'],
3951 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003952
3953 dups = self.get_duplicated_hosts()
3954 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003955 yield ('The following hosts were defined twice',
3956 self._format_hosts(dups),
3957 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003958
3959 partial = self.get_partially_configured_hosts()
3960 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003961 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3962 'These hosts are missing',
3963 self._format_hosts(partial, lambda host: 'but %s defined' %
3964 self._get_counterpart_host(host)),
3965 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003966
3967 conflicting = self.get_conflicting_hosts()
3968 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003969 yield ('The following Git hosts have differing credentials from their '
3970 'Gerrit counterparts',
3971 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3972 tuple(self._get_git_gerrit_identity_pairs()[host])),
3973 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003974
3975 wrong = self.get_hosts_with_wrong_identities()
3976 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003977 yield ('These hosts likely use wrong identity',
3978 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3979 (self._get_git_gerrit_identity_pairs()[host][0],
3980 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3981 wrong)
3982
3983 def find_and_report_problems(self):
3984 """Returns True if there was at least one problem, else False."""
3985 found = False
3986 bad_hosts = set()
3987 for title, sublines, hosts in self._find_problems():
3988 if not found:
3989 found = True
3990 print('\n\n.gitcookies problem report:\n')
3991 bad_hosts.update(hosts or [])
3992 print(' %s%s' % (title , (':' if sublines else '')))
3993 if sublines:
3994 print()
3995 print(' %s' % '\n '.join(sublines))
3996 print()
3997
3998 if bad_hosts:
3999 assert found
4000 print(' You can manually remove corresponding lines in your %s file and '
4001 'visit the following URLs with correct account to generate '
4002 'correct credential lines:\n' %
4003 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4004 print(' %s' % '\n '.join(sorted(set(
4005 gerrit_util.CookiesAuthenticator().get_new_password_url(
4006 self._canonical_git_googlesource_host(host))
4007 for host in bad_hosts
4008 ))))
4009 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004010
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004011
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004012@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004013def CMDcreds_check(parser, args):
4014 """Checks credentials and suggests changes."""
4015 _, _ = parser.parse_args(args)
4016
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004017 # Code below checks .gitcookies. Abort if using something else.
4018 authn = gerrit_util.Authenticator.get()
4019 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4020 if isinstance(authn, gerrit_util.GceAuthenticator):
4021 DieWithError(
4022 'This command is not designed for GCE, are you on a bot?\n'
4023 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4024 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004025 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004026 'This command is not designed for bot environment. It checks '
4027 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004028
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004029 checker = _GitCookiesChecker()
4030 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004031
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004032 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004033 checker.print_current_creds(include_netrc=True)
4034
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004035 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004036 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004037 return 0
4038 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004039
4040
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004041@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004042@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004044 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004045
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004046 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004047 # TODO(tandrii): remove this once we switch to Gerrit.
4048 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004049 parser.add_option('--activate-update', action='store_true',
4050 help='activate auto-updating [rietveld] section in '
4051 '.git/config')
4052 parser.add_option('--deactivate-update', action='store_true',
4053 help='deactivate auto-updating [rietveld] section in '
4054 '.git/config')
4055 options, args = parser.parse_args(args)
4056
4057 if options.deactivate_update:
4058 RunGit(['config', 'rietveld.autoupdate', 'false'])
4059 return
4060
4061 if options.activate_update:
4062 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4063 return
4064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004066 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067 return 0
4068
4069 url = args[0]
4070 if not url.endswith('codereview.settings'):
4071 url = os.path.join(url, 'codereview.settings')
4072
4073 # Load code review settings and download hooks (if available).
4074 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4075 return 0
4076
4077
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004078@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004079def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004080 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004081 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4082 branch = ShortBranchName(branchref)
4083 _, args = parser.parse_args(args)
4084 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004085 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004086 return RunGit(['config', 'branch.%s.base-url' % branch],
4087 error_ok=False).strip()
4088 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004090 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4091 error_ok=False).strip()
4092
4093
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004094def color_for_status(status):
4095 """Maps a Changelist status to color, for CMDstatus and other tools."""
4096 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004097 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004098 'waiting': Fore.BLUE,
4099 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004100 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004101 'lgtm': Fore.GREEN,
4102 'commit': Fore.MAGENTA,
4103 'closed': Fore.CYAN,
4104 'error': Fore.WHITE,
4105 }.get(status, Fore.WHITE)
4106
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004107
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004108def get_cl_statuses(changes, fine_grained, max_processes=None):
4109 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004110
4111 If fine_grained is true, this will fetch CL statuses from the server.
4112 Otherwise, simply indicate if there's a matching url for the given branches.
4113
4114 If max_processes is specified, it is used as the maximum number of processes
4115 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4116 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004117
4118 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004119 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004120 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004121 upload.verbosity = 0
4122
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004123 if not changes:
4124 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004125
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004126 if not fine_grained:
4127 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004128 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004129 for cl in changes:
4130 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004131 return
4132
4133 # First, sort out authentication issues.
4134 logging.debug('ensuring credentials exist')
4135 for cl in changes:
4136 cl.EnsureAuthenticated(force=False, refresh=True)
4137
4138 def fetch(cl):
4139 try:
4140 return (cl, cl.GetStatus())
4141 except:
4142 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004143 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004144 raise
4145
4146 threads_count = len(changes)
4147 if max_processes:
4148 threads_count = max(1, min(threads_count, max_processes))
4149 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4150
4151 pool = ThreadPool(threads_count)
4152 fetched_cls = set()
4153 try:
4154 it = pool.imap_unordered(fetch, changes).__iter__()
4155 while True:
4156 try:
4157 cl, status = it.next(timeout=5)
4158 except multiprocessing.TimeoutError:
4159 break
4160 fetched_cls.add(cl)
4161 yield cl, status
4162 finally:
4163 pool.close()
4164
4165 # Add any branches that failed to fetch.
4166 for cl in set(changes) - fetched_cls:
4167 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004168
rmistry@google.com2dd99862015-06-22 12:22:18 +00004169
4170def upload_branch_deps(cl, args):
4171 """Uploads CLs of local branches that are dependents of the current branch.
4172
4173 If the local branch dependency tree looks like:
4174 test1 -> test2.1 -> test3.1
4175 -> test3.2
4176 -> test2.2 -> test3.3
4177
4178 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4179 run on the dependent branches in this order:
4180 test2.1, test3.1, test3.2, test2.2, test3.3
4181
4182 Note: This function does not rebase your local dependent branches. Use it when
4183 you make a change to the parent branch that will not conflict with its
4184 dependent branches, and you would like their dependencies updated in
4185 Rietveld.
4186 """
4187 if git_common.is_dirty_git_tree('upload-branch-deps'):
4188 return 1
4189
4190 root_branch = cl.GetBranch()
4191 if root_branch is None:
4192 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4193 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004194 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004195 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4196 'patchset dependencies without an uploaded CL.')
4197
4198 branches = RunGit(['for-each-ref',
4199 '--format=%(refname:short) %(upstream:short)',
4200 'refs/heads'])
4201 if not branches:
4202 print('No local branches found.')
4203 return 0
4204
4205 # Create a dictionary of all local branches to the branches that are dependent
4206 # on it.
4207 tracked_to_dependents = collections.defaultdict(list)
4208 for b in branches.splitlines():
4209 tokens = b.split()
4210 if len(tokens) == 2:
4211 branch_name, tracked = tokens
4212 tracked_to_dependents[tracked].append(branch_name)
4213
vapiera7fbd5a2016-06-16 09:17:49 -07004214 print()
4215 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004216 dependents = []
4217 def traverse_dependents_preorder(branch, padding=''):
4218 dependents_to_process = tracked_to_dependents.get(branch, [])
4219 padding += ' '
4220 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004221 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004222 dependents.append(dependent)
4223 traverse_dependents_preorder(dependent, padding)
4224 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004225 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004226
4227 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004228 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004229 return 0
4230
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004231 confirm_or_exit('This command will checkout all dependent branches and run '
4232 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004233
rmistry@google.com2dd99862015-06-22 12:22:18 +00004234 # Record all dependents that failed to upload.
4235 failures = {}
4236 # Go through all dependents, checkout the branch and upload.
4237 try:
4238 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004239 print()
4240 print('--------------------------------------')
4241 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004242 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004243 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004244 try:
4245 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004246 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004247 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004248 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004249 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004251 finally:
4252 # Swap back to the original root branch.
4253 RunGit(['checkout', '-q', root_branch])
4254
vapiera7fbd5a2016-06-16 09:17:49 -07004255 print()
4256 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004257 for dependent_branch in dependents:
4258 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004259 print(' %s : %s' % (dependent_branch, upload_status))
4260 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004261
4262 return 0
4263
4264
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004265@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004266def CMDarchive(parser, args):
4267 """Archives and deletes branches associated with closed changelists."""
4268 parser.add_option(
4269 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004270 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004271 parser.add_option(
4272 '-f', '--force', action='store_true',
4273 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004274 parser.add_option(
4275 '-d', '--dry-run', action='store_true',
4276 help='Skip the branch tagging and removal steps.')
4277 parser.add_option(
4278 '-t', '--notags', action='store_true',
4279 help='Do not tag archived branches. '
4280 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004281
4282 auth.add_auth_options(parser)
4283 options, args = parser.parse_args(args)
4284 if args:
4285 parser.error('Unsupported args: %s' % ' '.join(args))
4286 auth_config = auth.extract_auth_config_from_options(options)
4287
4288 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4289 if not branches:
4290 return 0
4291
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004293 changes = [Changelist(branchref=b, auth_config=auth_config)
4294 for b in branches.splitlines()]
4295 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4296 statuses = get_cl_statuses(changes,
4297 fine_grained=True,
4298 max_processes=options.maxjobs)
4299 proposal = [(cl.GetBranch(),
4300 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4301 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004302 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004303 proposal.sort()
4304
4305 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004306 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004307 return 0
4308
4309 current_branch = GetCurrentBranch()
4310
vapiera7fbd5a2016-06-16 09:17:49 -07004311 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004312 if options.notags:
4313 for next_item in proposal:
4314 print(' ' + next_item[0])
4315 else:
4316 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4317 for next_item in proposal:
4318 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004319
kmarshall9249e012016-08-23 12:02:16 -07004320 # Quit now on precondition failure or if instructed by the user, either
4321 # via an interactive prompt or by command line flags.
4322 if options.dry_run:
4323 print('\nNo changes were made (dry run).\n')
4324 return 0
4325 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004326 print('You are currently on a branch \'%s\' which is associated with a '
4327 'closed codereview issue, so archive cannot proceed. Please '
4328 'checkout another branch and run this command again.' %
4329 current_branch)
4330 return 1
kmarshall9249e012016-08-23 12:02:16 -07004331 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004332 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4333 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004334 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004335 return 1
4336
4337 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004338 if not options.notags:
4339 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004340 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004341
vapiera7fbd5a2016-06-16 09:17:49 -07004342 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004343
4344 return 0
4345
4346
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004347@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004348def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004349 """Show status of changelists.
4350
4351 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004352 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004353 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004354 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004355 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004356 - Magenta in the commit queue
4357 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004358 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004359
4360 Also see 'git cl comments'.
4361 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004362 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004363 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004364 parser.add_option('-f', '--fast', action='store_true',
4365 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004366 parser.add_option(
4367 '-j', '--maxjobs', action='store', type=int,
4368 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004369
4370 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004371 _add_codereview_issue_select_options(
4372 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004373 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004374 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004375 if args:
4376 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004377 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004378
iannuccie53c9352016-08-17 14:40:40 -07004379 if options.issue is not None and not options.field:
4380 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004383 cl = Changelist(auth_config=auth_config, issue=options.issue,
4384 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004386 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 elif options.field == 'id':
4388 issueid = cl.GetIssue()
4389 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004390 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004392 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004393 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004394 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004395 elif options.field == 'status':
4396 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397 elif options.field == 'url':
4398 url = cl.GetIssueURL()
4399 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004400 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004401 return 0
4402
4403 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4404 if not branches:
4405 print('No local branch found.')
4406 return 0
4407
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004408 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004409 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004410 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004411 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004412 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004413 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004414 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004415
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004416 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004417 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4418 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4419 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004420 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004421 c, status = output.next()
4422 branch_statuses[c.GetBranch()] = status
4423 status = branch_statuses.pop(branch)
4424 url = cl.GetIssueURL()
4425 if url and (not status or status == 'error'):
4426 # The issue probably doesn't exist anymore.
4427 url += ' (broken)'
4428
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004429 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004430 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004431 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004432 color = ''
4433 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004434 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004435 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004436 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004437 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004438
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004439
4440 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004441 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004442 print('Current branch: %s' % branch)
4443 for cl in changes:
4444 if cl.GetBranch() == branch:
4445 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004446 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004447 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004448 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004449 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004450 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004451 print('Issue description:')
4452 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004453 return 0
4454
4455
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004456def colorize_CMDstatus_doc():
4457 """To be called once in main() to add colors to git cl status help."""
4458 colors = [i for i in dir(Fore) if i[0].isupper()]
4459
4460 def colorize_line(line):
4461 for color in colors:
4462 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004463 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004464 indent = len(line) - len(line.lstrip(' ')) + 1
4465 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4466 return line
4467
4468 lines = CMDstatus.__doc__.splitlines()
4469 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4470
4471
phajdan.jre328cf92016-08-22 04:12:17 -07004472def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004473 if path == '-':
4474 json.dump(contents, sys.stdout)
4475 else:
4476 with open(path, 'w') as f:
4477 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004478
4479
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004480@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004481@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004482def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004483 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004484
4485 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004486 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004487 parser.add_option('-r', '--reverse', action='store_true',
4488 help='Lookup the branch(es) for the specified issues. If '
4489 'no issues are specified, all branches with mapped '
4490 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004491 parser.add_option('--json',
4492 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004493 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004494 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004495 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496
dnj@chromium.org406c4402015-03-03 17:22:28 +00004497 if options.reverse:
4498 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004499 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004500 # Reverse issue lookup.
4501 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004502
4503 git_config = {}
4504 for config in RunGit(['config', '--get-regexp',
4505 r'branch\..*issue']).splitlines():
4506 name, _space, val = config.partition(' ')
4507 git_config[name] = val
4508
dnj@chromium.org406c4402015-03-03 17:22:28 +00004509 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004510 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4511 config_key = _git_branch_config_key(ShortBranchName(branch),
4512 cls.IssueConfigKey())
4513 issue = git_config.get(config_key)
4514 if issue:
4515 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004516 if not args:
4517 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004518 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004519 for issue in args:
4520 if not issue:
4521 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004522 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004523 print('Branch for issue number %s: %s' % (
4524 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004525 if options.json:
4526 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004527 return 0
4528
4529 if len(args) > 0:
4530 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4531 if not issue.valid:
4532 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4533 'or no argument to list it.\n'
4534 'Maybe you want to run git cl status?')
4535 cl = Changelist(codereview=issue.codereview)
4536 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004537 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004538 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004539 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4540 if options.json:
4541 write_json(options.json, {
4542 'issue': cl.GetIssue(),
4543 'issue_url': cl.GetIssueURL(),
4544 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545 return 0
4546
4547
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004548@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004549def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004550 """Shows or posts review comments for any changelist."""
4551 parser.add_option('-a', '--add-comment', dest='comment',
4552 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004553 parser.add_option('-p', '--publish', action='store_true',
4554 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004555 parser.add_option('-i', '--issue', dest='issue',
4556 help='review issue id (defaults to current issue). '
4557 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004558 parser.add_option('-m', '--machine-readable', dest='readable',
4559 action='store_false', default=True,
4560 help='output comments in a format compatible with '
4561 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004562 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004563 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004564 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004565 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004566 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004567 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004568 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004569
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004570 issue = None
4571 if options.issue:
4572 try:
4573 issue = int(options.issue)
4574 except ValueError:
4575 DieWithError('A review issue id is expected to be a number')
4576
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004577 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4578
4579 if not cl.IsGerrit():
4580 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004581
4582 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004583 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004584 return 0
4585
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004586 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4587 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004588 for comment in summary:
4589 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004590 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004591 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004592 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004593 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004594 color = Fore.MAGENTA
4595 else:
4596 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004597 print('\n%s%s %s%s\n%s' % (
4598 color,
4599 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4600 comment.sender,
4601 Fore.RESET,
4602 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4603
smut@google.comc85ac942015-09-15 16:34:43 +00004604 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004605 def pre_serialize(c):
4606 dct = c.__dict__.copy()
4607 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4608 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004609 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004610 return 0
4611
4612
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004613@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004614@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004615def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004616 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004617 parser.add_option('-d', '--display', action='store_true',
4618 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004619 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004620 help='New description to set for this issue (- for stdin, '
4621 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004622 parser.add_option('-f', '--force', action='store_true',
4623 help='Delete any unpublished Gerrit edits for this issue '
4624 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004625
4626 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004627 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004628 options, args = parser.parse_args(args)
4629 _process_codereview_select_options(parser, options)
4630
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004631 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004632 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004633 target_issue_arg = ParseIssueNumberArgument(args[0],
4634 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004635 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004636 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004637
martiniss6eda05f2016-06-30 10:18:35 -07004638 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004639 'auth_config': auth.extract_auth_config_from_options(options),
4640 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004641 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004642 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004643 if target_issue_arg:
4644 kwargs['issue'] = target_issue_arg.issue
4645 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004646 if target_issue_arg.codereview and not options.forced_codereview:
4647 detected_codereview_from_url = True
4648 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004649
4650 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004651 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004652 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004653 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004654
4655 if detected_codereview_from_url:
4656 logging.info('canonical issue/change URL: %s (type: %s)\n',
4657 cl.GetIssueURL(), target_issue_arg.codereview)
4658
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004659 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004660
smut@google.com34fb6b12015-07-13 20:03:26 +00004661 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004662 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004663 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004664
4665 if options.new_description:
4666 text = options.new_description
4667 if text == '-':
4668 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004669 elif text == '+':
4670 base_branch = cl.GetCommonAncestorWithUpstream()
4671 change = cl.GetChange(base_branch, None, local_description=True)
4672 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004673
4674 description.set_description(text)
4675 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004676 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004677
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004678 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004679 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004680 return 0
4681
4682
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004683@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004684def CMDlint(parser, args):
4685 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004686 parser.add_option('--filter', action='append', metavar='-x,+y',
4687 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004688 auth.add_auth_options(parser)
4689 options, args = parser.parse_args(args)
4690 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004691
4692 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004693 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004694 try:
4695 import cpplint
4696 import cpplint_chromium
4697 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004698 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004699 return 1
4700
4701 # Change the current working directory before calling lint so that it
4702 # shows the correct base.
4703 previous_cwd = os.getcwd()
4704 os.chdir(settings.GetRoot())
4705 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004706 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004707 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4708 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004709 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004710 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004711 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004712
4713 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004714 command = args + files
4715 if options.filter:
4716 command = ['--filter=' + ','.join(options.filter)] + command
4717 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004718
4719 white_regex = re.compile(settings.GetLintRegex())
4720 black_regex = re.compile(settings.GetLintIgnoreRegex())
4721 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4722 for filename in filenames:
4723 if white_regex.match(filename):
4724 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004725 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004726 else:
4727 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4728 extra_check_functions)
4729 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004730 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004731 finally:
4732 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004733 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004734 if cpplint._cpplint_state.error_count != 0:
4735 return 1
4736 return 0
4737
4738
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004739@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004740def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004741 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004742 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004743 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004744 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004745 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004746 parser.add_option('--all', action='store_true',
4747 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004748 parser.add_option('--parallel', action='store_true',
4749 help='Run all tests specified by input_api.RunTests in all '
4750 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004751 auth.add_auth_options(parser)
4752 options, args = parser.parse_args(args)
4753 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004754
sbc@chromium.org71437c02015-04-09 19:29:40 +00004755 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004756 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004757 return 1
4758
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004759 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760 if args:
4761 base_branch = args[0]
4762 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004763 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004764 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004765
Aaron Gable8076c282017-11-29 14:39:41 -08004766 if options.all:
4767 base_change = cl.GetChange(base_branch, None)
4768 files = [('M', f) for f in base_change.AllFiles()]
4769 change = presubmit_support.GitChange(
4770 base_change.Name(),
4771 base_change.FullDescriptionText(),
4772 base_change.RepositoryRoot(),
4773 files,
4774 base_change.issue,
4775 base_change.patchset,
4776 base_change.author_email,
4777 base_change._upstream)
4778 else:
4779 change = cl.GetChange(base_branch, None)
4780
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004781 cl.RunHook(
4782 committing=not options.upload,
4783 may_prompt=False,
4784 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004785 change=change,
4786 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004787 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788
4789
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004790def GenerateGerritChangeId(message):
4791 """Returns Ixxxxxx...xxx change id.
4792
4793 Works the same way as
4794 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4795 but can be called on demand on all platforms.
4796
4797 The basic idea is to generate git hash of a state of the tree, original commit
4798 message, author/committer info and timestamps.
4799 """
4800 lines = []
4801 tree_hash = RunGitSilent(['write-tree'])
4802 lines.append('tree %s' % tree_hash.strip())
4803 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4804 if code == 0:
4805 lines.append('parent %s' % parent.strip())
4806 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4807 lines.append('author %s' % author.strip())
4808 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4809 lines.append('committer %s' % committer.strip())
4810 lines.append('')
4811 # Note: Gerrit's commit-hook actually cleans message of some lines and
4812 # whitespace. This code is not doing this, but it clearly won't decrease
4813 # entropy.
4814 lines.append(message)
4815 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4816 stdin='\n'.join(lines))
4817 return 'I%s' % change_hash.strip()
4818
4819
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004820def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004821 """Computes the remote branch ref to use for the CL.
4822
4823 Args:
4824 remote (str): The git remote for the CL.
4825 remote_branch (str): The git remote branch for the CL.
4826 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004827 """
4828 if not (remote and remote_branch):
4829 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004830
wittman@chromium.org455dc922015-01-26 20:15:50 +00004831 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004832 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004833 # refs, which are then translated into the remote full symbolic refs
4834 # below.
4835 if '/' not in target_branch:
4836 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4837 else:
4838 prefix_replacements = (
4839 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4840 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4841 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4842 )
4843 match = None
4844 for regex, replacement in prefix_replacements:
4845 match = re.search(regex, target_branch)
4846 if match:
4847 remote_branch = target_branch.replace(match.group(0), replacement)
4848 break
4849 if not match:
4850 # This is a branch path but not one we recognize; use as-is.
4851 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004852 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4853 # Handle the refs that need to land in different refs.
4854 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004855
wittman@chromium.org455dc922015-01-26 20:15:50 +00004856 # Create the true path to the remote branch.
4857 # Does the following translation:
4858 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4859 # * refs/remotes/origin/master -> refs/heads/master
4860 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4861 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4862 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4863 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4864 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4865 'refs/heads/')
4866 elif remote_branch.startswith('refs/remotes/branch-heads'):
4867 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004868
wittman@chromium.org455dc922015-01-26 20:15:50 +00004869 return remote_branch
4870
4871
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004872def cleanup_list(l):
4873 """Fixes a list so that comma separated items are put as individual items.
4874
4875 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4876 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4877 """
4878 items = sum((i.split(',') for i in l), [])
4879 stripped_items = (i.strip() for i in items)
4880 return sorted(filter(None, stripped_items))
4881
4882
Aaron Gable4db38df2017-11-03 14:59:07 -07004883@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004884@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004885def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004886 """Uploads the current changelist to codereview.
4887
4888 Can skip dependency patchset uploads for a branch by running:
4889 git config branch.branch_name.skip-deps-uploads True
4890 To unset run:
4891 git config --unset branch.branch_name.skip-deps-uploads
4892 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004893
4894 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4895 a bug number, this bug number is automatically populated in the CL
4896 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004897
4898 If subject contains text in square brackets or has "<text>: " prefix, such
4899 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4900 [git-cl] add support for hashtags
4901 Foo bar: implement foo
4902 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004903 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004904 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4905 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004906 parser.add_option('--bypass-watchlists', action='store_true',
4907 dest='bypass_watchlists',
4908 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004909 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004910 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004911 parser.add_option('--message', '-m', dest='message',
4912 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004913 parser.add_option('-b', '--bug',
4914 help='pre-populate the bug number(s) for this issue. '
4915 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004916 parser.add_option('--message-file', dest='message_file',
4917 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004918 parser.add_option('--title', '-t', dest='title',
4919 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004920 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004921 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004922 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004923 parser.add_option('--tbrs',
4924 action='append', default=[],
4925 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004926 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004927 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004928 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004929 parser.add_option('--hashtag', dest='hashtags',
4930 action='append', default=[],
4931 help=('Gerrit hashtag for new CL; '
4932 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004933 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004934 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004935 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004936 help='tell the commit queue to commit this patchset; '
4937 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004938 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004939 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004940 metavar='TARGET',
4941 help='Apply CL to remote ref TARGET. ' +
4942 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004943 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004944 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004945 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004946 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004947 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004948 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004949 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4950 const='TBR', help='add a set of OWNERS to TBR')
4951 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4952 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004953 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4954 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004955 help='Send the patchset to do a CQ dry run right after '
4956 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004957 parser.add_option('--dependencies', action='store_true',
4958 help='Uploads CLs of all the local branches that depend on '
4959 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004960 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4961 help='Sends your change to the CQ after an approval. Only '
4962 'works on repos that have the Auto-Submit label '
4963 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004964 parser.add_option('--parallel', action='store_true',
4965 help='Run all tests specified by input_api.RunTests in all '
4966 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004967
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004968 parser.add_option('--no-autocc', action='store_true',
4969 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004970 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004971 help='Set the review private. This implies --no-autocc.')
4972
rmistry@google.com2dd99862015-06-22 12:22:18 +00004973 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004974 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004975 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004976 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004977 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004978 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004979
sbc@chromium.org71437c02015-04-09 19:29:40 +00004980 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004981 return 1
4982
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004983 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004984 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004985 options.cc = cleanup_list(options.cc)
4986
tandriib80458a2016-06-23 12:20:07 -07004987 if options.message_file:
4988 if options.message:
4989 parser.error('only one of --message and --message-file allowed.')
4990 options.message = gclient_utils.FileRead(options.message_file)
4991 options.message_file = None
4992
tandrii4d0545a2016-07-06 03:56:49 -07004993 if options.cq_dry_run and options.use_commit_queue:
4994 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4995
Aaron Gableedbc4132017-09-11 13:22:28 -07004996 if options.use_commit_queue:
4997 options.send_mail = True
4998
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004999 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5000 settings.GetIsGerrit()
5001
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005002 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005003 if not cl.IsGerrit():
5004 # Error out with instructions for repos not yet configured for Gerrit.
5005 print('=====================================')
5006 print('NOTICE: Rietveld is no longer supported. '
5007 'You can upload changes to Gerrit with')
5008 print(' git cl upload --gerrit')
5009 print('or set Gerrit to be your default code review tool with')
5010 print(' git config gerrit.host true')
5011 print('=====================================')
5012 return 1
5013
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005014 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005015
5016
Francois Dorayd42c6812017-05-30 15:10:20 -04005017@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005018@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005019def CMDsplit(parser, args):
5020 """Splits a branch into smaller branches and uploads CLs.
5021
5022 Creates a branch and uploads a CL for each group of files modified in the
5023 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005024 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005025 the shared OWNERS file.
5026 """
5027 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005028 help="A text file containing a CL description in which "
5029 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005030 parser.add_option("-c", "--comment", dest="comment_file",
5031 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005032 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5033 default=False,
5034 help="List the files and reviewers for each CL that would "
5035 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005036 parser.add_option("--cq-dry-run", action='store_true',
5037 help="If set, will do a cq dry run for each uploaded CL. "
5038 "Please be careful when doing this; more than ~10 CLs "
5039 "has the potential to overload our build "
5040 "infrastructure. Try to upload these not during high "
5041 "load times (usually 11-3 Mountain View time). Email "
5042 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005043 options, _ = parser.parse_args(args)
5044
5045 if not options.description_file:
5046 parser.error('No --description flag specified.')
5047
5048 def WrappedCMDupload(args):
5049 return CMDupload(OptionParser(), args)
5050
5051 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005052 Changelist, WrappedCMDupload, options.dry_run,
5053 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005054
5055
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005056@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005057@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005058def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005059 """DEPRECATED: Used to commit the current changelist via git-svn."""
5060 message = ('git-cl no longer supports committing to SVN repositories via '
5061 'git-svn. You probably want to use `git cl land` instead.')
5062 print(message)
5063 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005064
5065
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005066# Two special branches used by git cl land.
5067MERGE_BRANCH = 'git-cl-commit'
5068CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5069
5070
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005071@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005072@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005073def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005074 """Commits the current changelist via git.
5075
5076 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5077 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005078 """
5079 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5080 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005081 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005082 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005083 parser.add_option('--parallel', action='store_true',
5084 help='Run all tests specified by input_api.RunTests in all '
5085 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005086 auth.add_auth_options(parser)
5087 (options, args) = parser.parse_args(args)
5088 auth_config = auth.extract_auth_config_from_options(options)
5089
5090 cl = Changelist(auth_config=auth_config)
5091
Robert Iannucci2e73d432018-03-14 01:10:47 -07005092 if not cl.IsGerrit():
5093 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005094
Robert Iannucci2e73d432018-03-14 01:10:47 -07005095 if not cl.GetIssue():
5096 DieWithError('You must upload the change first to Gerrit.\n'
5097 ' If you would rather have `git cl land` upload '
5098 'automatically for you, see http://crbug.com/642759')
5099 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005100 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005101
5102
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005103@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005104@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005105def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005106 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005107 parser.add_option('-b', dest='newbranch',
5108 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005109 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005110 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005111 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005112 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005113 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005114 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005115 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005116 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005117 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005118 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005119
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005120
5121 group = optparse.OptionGroup(
5122 parser,
5123 'Options for continuing work on the current issue uploaded from a '
5124 'different clone (e.g. different machine). Must be used independently '
5125 'from the other options. No issue number should be specified, and the '
5126 'branch must have an issue number associated with it')
5127 group.add_option('--reapply', action='store_true', dest='reapply',
5128 help='Reset the branch and reapply the issue.\n'
5129 'CAUTION: This will undo any local changes in this '
5130 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005131
5132 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005133 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005134 parser.add_option_group(group)
5135
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005136 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005137 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005138 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005139 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005140 auth_config = auth.extract_auth_config_from_options(options)
5141
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005142 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005143 if options.newbranch:
5144 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005145 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005146 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005147
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005148 cl = Changelist(auth_config=auth_config,
5149 codereview=options.forced_codereview)
5150 if not cl.GetIssue():
5151 parser.error('current branch must have an associated issue')
5152
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005153 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005154 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005155 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005156
5157 RunGit(['reset', '--hard', upstream])
5158 if options.pull:
5159 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005160
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005161 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5162 options.directory)
5163
5164 if len(args) != 1 or not args[0]:
5165 parser.error('Must specify issue number or url')
5166
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005167 target_issue_arg = ParseIssueNumberArgument(args[0],
5168 options.forced_codereview)
5169 if not target_issue_arg.valid:
5170 parser.error('invalid codereview url or CL id')
5171
5172 cl_kwargs = {
5173 'auth_config': auth_config,
5174 'codereview_host': target_issue_arg.hostname,
5175 'codereview': options.forced_codereview,
5176 }
5177 detected_codereview_from_url = False
5178 if target_issue_arg.codereview and not options.forced_codereview:
5179 detected_codereview_from_url = True
5180 cl_kwargs['codereview'] = target_issue_arg.codereview
5181 cl_kwargs['issue'] = target_issue_arg.issue
5182
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005183 # We don't want uncommitted changes mixed up with the patch.
5184 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005185 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005186
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005187 if options.newbranch:
5188 if options.force:
5189 RunGit(['branch', '-D', options.newbranch],
5190 stderr=subprocess2.PIPE, error_ok=True)
5191 RunGit(['new-branch', options.newbranch])
5192
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005193 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005194
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005195 if cl.IsGerrit():
5196 if options.reject:
5197 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005198 if options.directory:
5199 parser.error('--directory is not supported with Gerrit codereview.')
5200
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005201 if detected_codereview_from_url:
5202 print('canonical issue/change URL: %s (type: %s)\n' %
5203 (cl.GetIssueURL(), target_issue_arg.codereview))
5204
5205 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005206 options.nocommit, options.directory,
5207 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005208
5209
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005210def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005211 """Fetches the tree status and returns either 'open', 'closed',
5212 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005213 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005214 if url:
5215 status = urllib2.urlopen(url).read().lower()
5216 if status.find('closed') != -1 or status == '0':
5217 return 'closed'
5218 elif status.find('open') != -1 or status == '1':
5219 return 'open'
5220 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005221 return 'unset'
5222
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005224def GetTreeStatusReason():
5225 """Fetches the tree status from a json url and returns the message
5226 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005227 url = settings.GetTreeStatusUrl()
5228 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005229 connection = urllib2.urlopen(json_url)
5230 status = json.loads(connection.read())
5231 connection.close()
5232 return status['message']
5233
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005234
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005235@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005236def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005237 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005238 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005239 status = GetTreeStatus()
5240 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005241 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005242 return 2
5243
vapiera7fbd5a2016-06-16 09:17:49 -07005244 print('The tree is %s' % status)
5245 print()
5246 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005247 if status != 'open':
5248 return 1
5249 return 0
5250
5251
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005252@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005253def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005254 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005255 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005256 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005257 '-b', '--bot', action='append',
5258 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5259 'times to specify multiple builders. ex: '
5260 '"-b win_rel -b win_layout". See '
5261 'the try server waterfall for the builders name and the tests '
5262 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005263 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005264 '-B', '--bucket', default='',
5265 help=('Buildbucket bucket to send the try requests.'))
5266 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005267 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005268 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005269 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005270 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005271 help='Revision to use for the try job; default: the revision will '
5272 'be determined by the try recipe that builder runs, which usually '
5273 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005274 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005275 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005276 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005277 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005278 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005279 '--category', default='git_cl_try', help='Specify custom build category.')
5280 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005281 '--project',
5282 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005283 'in recipe to determine to which repository or directory to '
5284 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005285 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005286 '-p', '--property', dest='properties', action='append', default=[],
5287 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005288 'key2=value2 etc. The value will be treated as '
5289 'json if decodable, or as string otherwise. '
5290 'NOTE: using this may make your try job not usable for CQ, '
5291 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005292 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005293 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5294 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005295 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005296 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005297 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005298 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005299 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005300 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005301
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005302 if options.master and options.master.startswith('luci.'):
5303 parser.error(
5304 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005305 # Make sure that all properties are prop=value pairs.
5306 bad_params = [x for x in options.properties if '=' not in x]
5307 if bad_params:
5308 parser.error('Got properties with missing "=": %s' % bad_params)
5309
maruel@chromium.org15192402012-09-06 12:38:29 +00005310 if args:
5311 parser.error('Unknown arguments: %s' % args)
5312
Koji Ishii31c14782018-01-08 17:17:33 +09005313 cl = Changelist(auth_config=auth_config, issue=options.issue,
5314 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005315 if not cl.GetIssue():
5316 parser.error('Need to upload first')
5317
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005318 if cl.IsGerrit():
5319 # HACK: warm up Gerrit change detail cache to save on RPCs.
5320 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5321
tandriie113dfd2016-10-11 10:20:12 -07005322 error_message = cl.CannotTriggerTryJobReason()
5323 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005324 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005325
borenet6c0efe62016-10-19 08:13:29 -07005326 if options.bucket and options.master:
5327 parser.error('Only one of --bucket and --master may be used.')
5328
qyearsley1fdfcb62016-10-24 13:22:03 -07005329 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005330
qyearsleydd49f942016-10-28 11:57:22 -07005331 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5332 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005333 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005334 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005335 print('git cl try with no bots now defaults to CQ dry run.')
5336 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5337 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005338
borenet6c0efe62016-10-19 08:13:29 -07005339 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005340 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005341 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005342 'of bot requires an initial job from a parent (usually a builder). '
5343 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005344 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005345 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005346
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005347 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005348 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005349 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005350 except BuildbucketResponseException as ex:
5351 print('ERROR: %s' % ex)
5352 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005353 return 0
5354
5355
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005356@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005357def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005358 """Prints info about try jobs associated with current CL."""
5359 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005360 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005361 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005362 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005363 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005364 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005365 '--color', action='store_true', default=setup_color.IS_TTY,
5366 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005367 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005368 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5369 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005370 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005371 '--json', help=('Path of JSON output file to write try job results to,'
5372 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005373 parser.add_option_group(group)
5374 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005375 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005376 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005377 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005378 if args:
5379 parser.error('Unrecognized args: %s' % ' '.join(args))
5380
5381 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005382 cl = Changelist(
5383 issue=options.issue, codereview=options.forced_codereview,
5384 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005385 if not cl.GetIssue():
5386 parser.error('Need to upload first')
5387
tandrii221ab252016-10-06 08:12:04 -07005388 patchset = options.patchset
5389 if not patchset:
5390 patchset = cl.GetMostRecentPatchset()
5391 if not patchset:
5392 parser.error('Codereview doesn\'t know about issue %s. '
5393 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005394 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005395 cl.GetIssue())
5396
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005397 try:
tandrii221ab252016-10-06 08:12:04 -07005398 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005399 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005400 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005401 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005402 if options.json:
5403 write_try_results_json(options.json, jobs)
5404 else:
5405 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005406 return 0
5407
5408
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005409@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005410@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005411def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005412 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005413 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005414 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005415 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005416
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005417 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005418 if args:
5419 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005420 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005421 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005422 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005423 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005424
5425 # Clear configured merge-base, if there is one.
5426 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005427 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005428 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005429 return 0
5430
5431
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005432@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005433def CMDweb(parser, args):
5434 """Opens the current CL in the web browser."""
5435 _, args = parser.parse_args(args)
5436 if args:
5437 parser.error('Unrecognized args: %s' % ' '.join(args))
5438
5439 issue_url = Changelist().GetIssueURL()
5440 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005441 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005442 return 1
5443
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005444 # Redirect I/O before invoking browser to hide its output. For example, this
5445 # allows to hide "Created new window in existing browser session." message
5446 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5447 saved_stdout = os.dup(1)
5448 os.close(1)
5449 os.open(os.devnull, os.O_RDWR)
5450 try:
5451 webbrowser.open(issue_url)
5452 finally:
5453 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005454 return 0
5455
5456
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005457@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005458def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005459 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005460 parser.add_option('-d', '--dry-run', action='store_true',
5461 help='trigger in dry run mode')
5462 parser.add_option('-c', '--clear', action='store_true',
5463 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005464 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005465 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005466 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005467 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005468 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005469 if args:
5470 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005471 if options.dry_run and options.clear:
5472 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5473
iannuccie53c9352016-08-17 14:40:40 -07005474 cl = Changelist(auth_config=auth_config, issue=options.issue,
5475 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005476 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005477 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005478 elif options.dry_run:
5479 state = _CQState.DRY_RUN
5480 else:
5481 state = _CQState.COMMIT
5482 if not cl.GetIssue():
5483 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005484 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005485 return 0
5486
5487
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005488@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005489def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005490 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005491 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005492 auth.add_auth_options(parser)
5493 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005494 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005495 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005496 if args:
5497 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005498 cl = Changelist(auth_config=auth_config, issue=options.issue,
5499 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005500 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005501 if not cl.GetIssue():
5502 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005503 cl.CloseIssue()
5504 return 0
5505
5506
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005507@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005508def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005509 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005510 parser.add_option(
5511 '--stat',
5512 action='store_true',
5513 dest='stat',
5514 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005515 auth.add_auth_options(parser)
5516 options, args = parser.parse_args(args)
5517 auth_config = auth.extract_auth_config_from_options(options)
5518 if args:
5519 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005520
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005521 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005522 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005523 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005524 if not issue:
5525 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005526
Aaron Gablea718c3e2017-08-28 17:47:28 -07005527 base = cl._GitGetBranchConfigValue('last-upload-hash')
5528 if not base:
5529 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5530 if not base:
5531 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5532 revision_info = detail['revisions'][detail['current_revision']]
5533 fetch_info = revision_info['fetch']['http']
5534 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5535 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005536
Aaron Gablea718c3e2017-08-28 17:47:28 -07005537 cmd = ['git', 'diff']
5538 if options.stat:
5539 cmd.append('--stat')
5540 cmd.append(base)
5541 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005542
5543 return 0
5544
5545
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005546@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005547def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005548 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005549 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005550 '--ignore-current',
5551 action='store_true',
5552 help='Ignore the CL\'s current reviewers and start from scratch.')
5553 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005554 '--no-color',
5555 action='store_true',
5556 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005557 parser.add_option(
5558 '--batch',
5559 action='store_true',
5560 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005561 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005562 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005563 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005564
5565 author = RunGit(['config', 'user.email']).strip() or None
5566
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005567 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005568
5569 if args:
5570 if len(args) > 1:
5571 parser.error('Unknown args')
5572 base_branch = args[0]
5573 else:
5574 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005575 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005576
5577 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005578 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5579
5580 if options.batch:
5581 db = owners.Database(change.RepositoryRoot(), file, os.path)
5582 print('\n'.join(db.reviewers_for(affected_files, author)))
5583 return 0
5584
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005585 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005586 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005587 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005588 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005589 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005590 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005591 disable_color=options.no_color,
5592 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005593
5594
Aiden Bennerc08566e2018-10-03 17:52:42 +00005595def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005596 """Generates a diff command."""
5597 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005598 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5599
5600 if not allow_prefix:
5601 diff_cmd += ['--no-prefix']
5602
5603 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005604
5605 if args:
5606 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005607 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005608 diff_cmd.append(arg)
5609 else:
5610 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005611
5612 return diff_cmd
5613
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005614
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005615def MatchingFileType(file_name, extensions):
5616 """Returns true if the file name ends with one of the given extensions."""
5617 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005618
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005619
enne@chromium.org555cfe42014-01-29 18:21:39 +00005620@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005621@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005622def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005623 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005624 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005625 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005626 parser.add_option('--full', action='store_true',
5627 help='Reformat the full content of all touched files')
5628 parser.add_option('--dry-run', action='store_true',
5629 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005630 parser.add_option('--python', action='store_true',
5631 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005632 parser.add_option('--js', action='store_true',
5633 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005634 parser.add_option('--diff', action='store_true',
5635 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005636 parser.add_option('--presubmit', action='store_true',
5637 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005638 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005639
Daniel Chengc55eecf2016-12-30 03:11:02 -08005640 # Normalize any remaining args against the current path, so paths relative to
5641 # the current directory are still resolved as expected.
5642 args = [os.path.join(os.getcwd(), arg) for arg in args]
5643
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005644 # git diff generates paths against the root of the repository. Change
5645 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005646 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005647 if rel_base_path:
5648 os.chdir(rel_base_path)
5649
digit@chromium.org29e47272013-05-17 17:01:46 +00005650 # Grab the merge-base commit, i.e. the upstream commit of the current
5651 # branch when it was created or the last time it was rebased. This is
5652 # to cover the case where the user may have called "git fetch origin",
5653 # moving the origin branch to a newer commit, but hasn't rebased yet.
5654 upstream_commit = None
5655 cl = Changelist()
5656 upstream_branch = cl.GetUpstreamBranch()
5657 if upstream_branch:
5658 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5659 upstream_commit = upstream_commit.strip()
5660
5661 if not upstream_commit:
5662 DieWithError('Could not find base commit for this branch. '
5663 'Are you in detached state?')
5664
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005665 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5666 diff_output = RunGit(changed_files_cmd)
5667 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005668 # Filter out files deleted by this CL
5669 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005670
Christopher Lamc5ba6922017-01-24 11:19:14 +11005671 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005672 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005673
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005674 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5675 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5676 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005677 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005678
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005679 top_dir = os.path.normpath(
5680 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5681
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005682 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5683 # formatted. This is used to block during the presubmit.
5684 return_value = 0
5685
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005686 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005687 # Locate the clang-format binary in the checkout
5688 try:
5689 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005690 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005691 DieWithError(e)
5692
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005693 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005694 cmd = [clang_format_tool]
5695 if not opts.dry_run and not opts.diff:
5696 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005697 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005698 if opts.diff:
5699 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005700 else:
5701 env = os.environ.copy()
5702 env['PATH'] = str(os.path.dirname(clang_format_tool))
5703 try:
5704 script = clang_format.FindClangFormatScriptInChromiumTree(
5705 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005706 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005707 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005708
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005709 cmd = [sys.executable, script, '-p0']
5710 if not opts.dry_run and not opts.diff:
5711 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005712
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005713 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5714 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005715
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005716 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5717 if opts.diff:
5718 sys.stdout.write(stdout)
5719 if opts.dry_run and len(stdout) > 0:
5720 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005721
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005722 # Similar code to above, but using yapf on .py files rather than clang-format
5723 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00005724 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005725 yapf_tool = gclient_utils.FindExecutable('yapf')
5726 if yapf_tool is None:
5727 DieWithError('yapf not found in PATH')
5728
Aiden Bennerc08566e2018-10-03 17:52:42 +00005729 # If we couldn't find a yapf file we'll default to the chromium style
5730 # specified in depot_tools.
5731 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5732 chromium_default_yapf_style = os.path.join(depot_tools_path,
5733 YAPF_CONFIG_FILENAME)
5734
5735 # Note: yapf still seems to fix indentation of the entire file
5736 # even if line ranges are specified.
5737 # See https://github.com/google/yapf/issues/499
5738 if not opts.full:
5739 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
5740
5741 # Used for caching.
5742 yapf_configs = {}
5743 for f in python_diff_files:
5744 # Find the yapf style config for the current file, defaults to depot
5745 # tools default.
5746 yapf_config = _FindYapfConfigFile(
5747 os.path.abspath(f), yapf_configs, top_dir,
5748 chromium_default_yapf_style)
5749
5750 cmd = [yapf_tool, '--style', yapf_config, f]
5751
5752 has_formattable_lines = False
5753 if not opts.full:
5754 # Only run yapf over changed line ranges.
5755 for diff_start, diff_len in py_line_diffs[f]:
5756 diff_end = diff_start + diff_len - 1
5757 # Yapf errors out if diff_end < diff_start but this
5758 # is a valid line range diff for a removal.
5759 if diff_end >= diff_start:
5760 has_formattable_lines = True
5761 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5762 # If all line diffs were removals we have nothing to format.
5763 if not has_formattable_lines:
5764 continue
5765
5766 if opts.diff or opts.dry_run:
5767 cmd += ['--diff']
5768 # Will return non-zero exit code if non-empty diff.
5769 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5770 if opts.diff:
5771 sys.stdout.write(stdout)
5772 elif len(stdout) > 0:
5773 return_value = 2
5774 else:
5775 cmd += ['-i']
5776 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005777
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005778 # Dart's formatter does not have the nice property of only operating on
5779 # modified chunks, so hard code full.
5780 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005781 try:
5782 command = [dart_format.FindDartFmtToolInChromiumTree()]
5783 if not opts.dry_run and not opts.diff:
5784 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005785 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005786
ppi@chromium.org6593d932016-03-03 15:41:15 +00005787 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005788 if opts.dry_run and stdout:
5789 return_value = 2
5790 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005791 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5792 'found in this checkout. Files in other languages are still '
5793 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005794
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005795 # Format GN build files. Always run on full build files for canonical form.
5796 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005797 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005798 if opts.dry_run or opts.diff:
5799 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005800 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005801 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5802 shell=sys.platform == 'win32',
5803 cwd=top_dir)
5804 if opts.dry_run and gn_ret == 2:
5805 return_value = 2 # Not formatted.
5806 elif opts.diff and gn_ret == 2:
5807 # TODO this should compute and print the actual diff.
5808 print("This change has GN build file diff for " + gn_diff_file)
5809 elif gn_ret != 0:
5810 # For non-dry run cases (and non-2 return values for dry-run), a
5811 # nonzero error code indicates a failure, probably because the file
5812 # doesn't parse.
5813 DieWithError("gn format failed on " + gn_diff_file +
5814 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005815
Ilya Shermane081cbe2017-08-15 17:51:04 -07005816 # Skip the metrics formatting from the global presubmit hook. These files have
5817 # a separate presubmit hook that issues an error if the files need formatting,
5818 # whereas the top-level presubmit script merely issues a warning. Formatting
5819 # these files is somewhat slow, so it's important not to duplicate the work.
5820 if not opts.presubmit:
5821 for xml_dir in GetDirtyMetricsDirs(diff_files):
5822 tool_dir = os.path.join(top_dir, xml_dir)
5823 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5824 if opts.dry_run or opts.diff:
5825 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005826 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005827 if opts.diff:
5828 sys.stdout.write(stdout)
5829 if opts.dry_run and stdout:
5830 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005831
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005832 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005833
Steven Holte2e664bf2017-04-21 13:10:47 -07005834def GetDirtyMetricsDirs(diff_files):
5835 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5836 metrics_xml_dirs = [
5837 os.path.join('tools', 'metrics', 'actions'),
5838 os.path.join('tools', 'metrics', 'histograms'),
5839 os.path.join('tools', 'metrics', 'rappor'),
5840 os.path.join('tools', 'metrics', 'ukm')]
5841 for xml_dir in metrics_xml_dirs:
5842 if any(file.startswith(xml_dir) for file in xml_diff_files):
5843 yield xml_dir
5844
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005845
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005846@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005847@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005848def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005849 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005850 _, args = parser.parse_args(args)
5851
5852 if len(args) != 1:
5853 parser.print_help()
5854 return 1
5855
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005856 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005857 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005858 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005859
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005860 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005861
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005862 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005863 output = RunGit(['config', '--local', '--get-regexp',
5864 r'branch\..*\.%s' % issueprefix],
5865 error_ok=True)
5866 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005867 if issue == target_issue:
5868 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005869
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005870 branches = []
5871 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005872 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005873 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005874 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005875 return 1
5876 if len(branches) == 1:
5877 RunGit(['checkout', branches[0]])
5878 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005879 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005880 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005881 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005882 which = raw_input('Choose by index: ')
5883 try:
5884 RunGit(['checkout', branches[int(which)]])
5885 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005886 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005887 return 1
5888
5889 return 0
5890
5891
maruel@chromium.org29404b52014-09-08 22:58:00 +00005892def CMDlol(parser, args):
5893 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005894 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005895 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5896 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5897 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005898 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005899 return 0
5900
5901
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005902class OptionParser(optparse.OptionParser):
5903 """Creates the option parse and add --verbose support."""
5904 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005905 optparse.OptionParser.__init__(
5906 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005907 self.add_option(
5908 '-v', '--verbose', action='count', default=0,
5909 help='Use 2 times for more debugging info')
5910
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005911 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005912 try:
5913 return self._parse_args(args)
5914 finally:
5915 # Regardless of success or failure of args parsing, we want to report
5916 # metrics, but only after logging has been initialized (if parsing
5917 # succeeded).
5918 global settings
5919 settings = Settings()
5920
5921 if not metrics.DISABLE_METRICS_COLLECTION:
5922 # GetViewVCUrl ultimately calls logging method.
5923 project_url = settings.GetViewVCUrl().strip('/+')
5924 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5925 metrics.collector.add('project_urls', [project_url])
5926
5927 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005928 # Create an optparse.Values object that will store only the actual passed
5929 # options, without the defaults.
5930 actual_options = optparse.Values()
5931 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5932 # Create an optparse.Values object with the default options.
5933 options = optparse.Values(self.get_default_values().__dict__)
5934 # Update it with the options passed by the user.
5935 options._update_careful(actual_options.__dict__)
5936 # Store the options passed by the user in an _actual_options attribute.
5937 # We store only the keys, and not the values, since the values can contain
5938 # arbitrary information, which might be PII.
5939 metrics.collector.add('arguments', actual_options.__dict__.keys())
5940
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005941 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005942 logging.basicConfig(
5943 level=levels[min(options.verbose, len(levels) - 1)],
5944 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5945 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005946
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005947 return options, args
5948
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005949
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005950def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005951 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005952 print('\nYour python version %s is unsupported, please upgrade.\n' %
5953 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005954 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005955
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005956 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005957 dispatcher = subcommand.CommandDispatcher(__name__)
5958 try:
5959 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005960 except auth.AuthenticationError as e:
5961 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005962 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005963 if e.code != 500:
5964 raise
5965 DieWithError(
5966 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5967 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005968 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005969
5970
5971if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005972 # These affect sys.stdout so do it outside of main() to simplify mocks in
5973 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005974 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005975 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005976 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005977 sys.exit(main(sys.argv[1:]))