blob: 8b957eb77d4fbea03bc7a3051e3e830cdf255a41 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070072DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080073POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000075REFS_THAT_ALIAS_TO_OTHER_REFS = {
76 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
77 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
78}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
thestig@chromium.org44202a22014-03-11 19:22:18 +000080# Valid extensions for files we want to lint.
81DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
82DEFAULT_LINT_IGNORE_REGEX = r"$^"
83
Aiden Bennerc08566e2018-10-03 17:52:42 +000084# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 08:13:29 -070087# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000090# Shortcut since it quickly becomes redundant.
91Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000092
maruel@chromium.orgddd59412011-11-30 14:20:38 +000093# Initialized in main()
94settings = None
95
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096# Used by tests/git_cl_test.py to add extra logging.
97# Inside the weirdly failing test, add this:
98# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070099# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100100_IS_BEING_TESTED = False
101
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000102
Christopher Lamf732cd52017-01-24 12:40:11 +1100103def DieWithError(message, change_desc=None):
104 if change_desc:
105 SaveDescriptionBackup(change_desc)
106
vapiera7fbd5a2016-06-16 09:17:49 -0700107 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108 sys.exit(1)
109
110
Christopher Lamf732cd52017-01-24 12:40:11 +1100111def SaveDescriptionBackup(change_desc):
112 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000113 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100114 backup_file = open(backup_path, 'w')
115 backup_file.write(change_desc.description)
116 backup_file.close()
117
118
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119def GetNoGitPagerEnv():
120 env = os.environ.copy()
121 # 'cat' is a magical git string that disables pagers on all platforms.
122 env['GIT_PAGER'] = 'cat'
123 return env
124
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000125
bsep@chromium.org627d9002016-04-29 00:00:52 +0000126def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000128 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000129 except subprocess2.CalledProcessError as e:
130 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000131 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000133 'Command "%s" failed.\n%s' % (
134 ' '.join(args), error_message or e.stdout or ''))
135 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136
137
138def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000139 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000140 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000141
142
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000143def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000144 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700145 if suppress_stderr:
146 stderr = subprocess2.VOID
147 else:
148 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000149 try:
tandrii5d48c322016-08-18 16:19:37 -0700150 (out, _), code = subprocess2.communicate(['git'] + args,
151 env=GetNoGitPagerEnv(),
152 stdout=subprocess2.PIPE,
153 stderr=stderr)
154 return code, out
155 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900156 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700157 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158
159
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000160def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000161 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000162 return RunGitWithCode(args, suppress_stderr=True)[1]
163
164
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000165def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000166 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000168 return (version.startswith(prefix) and
169 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000170
171
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000172def BranchExists(branch):
173 """Return True if specified branch exists."""
174 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
175 suppress_stderr=True)
176 return not code
177
178
tandrii2a16b952016-10-19 07:09:44 -0700179def time_sleep(seconds):
180 # Use this so that it can be mocked in tests without interfering with python
181 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700182 return time.sleep(seconds)
183
184
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000185def time_time():
186 # Use this so that it can be mocked in tests without interfering with python
187 # system machinery.
188 return time.time()
189
190
maruel@chromium.org90541732011-04-01 17:54:18 +0000191def ask_for_data(prompt):
192 try:
193 return raw_input(prompt)
194 except KeyboardInterrupt:
195 # Hide the exception.
196 sys.exit(1)
197
198
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100199def confirm_or_exit(prefix='', action='confirm'):
200 """Asks user to press enter to continue or press Ctrl+C to abort."""
201 if not prefix or prefix.endswith('\n'):
202 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100203 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100204 mid = ' Press'
205 elif prefix.endswith(' '):
206 mid = 'press'
207 else:
208 mid = ' press'
209 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
210
211
212def ask_for_explicit_yes(prompt):
213 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
214 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
215 while True:
216 if 'yes'.startswith(result):
217 return True
218 if 'no'.startswith(result):
219 return False
220 result = ask_for_data('Please, type yes or no: ').lower()
221
222
tandrii5d48c322016-08-18 16:19:37 -0700223def _git_branch_config_key(branch, key):
224 """Helper method to return Git config key for a branch."""
225 assert branch, 'branch name is required to set git config for it'
226 return 'branch.%s.%s' % (branch, key)
227
228
229def _git_get_branch_config_value(key, default=None, value_type=str,
230 branch=False):
231 """Returns git config value of given or current branch if any.
232
233 Returns default in all other cases.
234 """
235 assert value_type in (int, str, bool)
236 if branch is False: # Distinguishing default arg value from None.
237 branch = GetCurrentBranch()
238
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000239 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700240 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000241
tandrii5d48c322016-08-18 16:19:37 -0700242 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700243 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700244 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700245 # git config also has --int, but apparently git config suffers from integer
246 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700247 args.append(_git_branch_config_key(branch, key))
248 code, out = RunGitWithCode(args)
249 if code == 0:
250 value = out.strip()
251 if value_type == int:
252 return int(value)
253 if value_type == bool:
254 return bool(value.lower() == 'true')
255 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000256 return default
257
258
tandrii5d48c322016-08-18 16:19:37 -0700259def _git_set_branch_config_value(key, value, branch=None, **kwargs):
260 """Sets the value or unsets if it's None of a git branch config.
261
262 Valid, though not necessarily existing, branch must be provided,
263 otherwise currently checked out branch is used.
264 """
265 if not branch:
266 branch = GetCurrentBranch()
267 assert branch, 'a branch name OR currently checked out branch is required'
268 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700269 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700270 if value is None:
271 args.append('--unset')
272 elif isinstance(value, bool):
273 args.append('--bool')
274 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700275 else:
tandrii33a46ff2016-08-23 05:53:40 -0700276 # git config also has --int, but apparently git config suffers from integer
277 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700278 value = str(value)
279 args.append(_git_branch_config_key(branch, key))
280 if value is not None:
281 args.append(value)
282 RunGit(args, **kwargs)
283
284
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100285def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700286 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100287
288 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
289 """
290 # Git also stores timezone offset, but it only affects visual display,
291 # actual point in time is defined by this timestamp only.
292 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
293
294
295def _git_amend_head(message, committer_timestamp):
296 """Amends commit with new message and desired committer_timestamp.
297
298 Sets committer timezone to UTC.
299 """
300 env = os.environ.copy()
301 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
302 return RunGit(['commit', '--amend', '-m', message], env=env)
303
304
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305def _get_properties_from_options(options):
306 properties = dict(x.split('=', 1) for x in options.properties)
307 for key, val in properties.iteritems():
308 try:
309 properties[key] = json.loads(val)
310 except ValueError:
311 pass # If a value couldn't be evaluated, treat it as a string.
312 return properties
313
314
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000315def _prefix_master(master):
316 """Convert user-specified master name to full master name.
317
318 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
319 name, while the developers always use shortened master name
320 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
321 function does the conversion for buildbucket migration.
322 """
borenet6c0efe62016-10-19 08:13:29 -0700323 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 return master
borenet6c0efe62016-10-19 08:13:29 -0700325 return '%s%s' % (MASTER_PREFIX, master)
326
327
328def _unprefix_master(bucket):
329 """Convert bucket name to shortened master name.
330
331 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
332 name, while the developers always use shortened master name
333 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
334 function does the conversion for buildbucket migration.
335 """
336 if bucket.startswith(MASTER_PREFIX):
337 return bucket[len(MASTER_PREFIX):]
338 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339
340
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000341def _buildbucket_retry(operation_name, http, *args, **kwargs):
342 """Retries requests to buildbucket service and returns parsed json content."""
343 try_count = 0
344 while True:
345 response, content = http.request(*args, **kwargs)
346 try:
347 content_json = json.loads(content)
348 except ValueError:
349 content_json = None
350
351 # Buildbucket could return an error even if status==200.
352 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000353 error = content_json.get('error')
354 if error.get('code') == 403:
355 raise BuildbucketResponseException(
356 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000357 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000358 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 raise BuildbucketResponseException(msg)
360
361 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700362 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000363 raise BuildbucketResponseException(
364 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700365 'Please file bugs at http://crbug.com, '
366 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367 content)
368 return content_json
369 if response.status < 500 or try_count >= 2:
370 raise httplib2.HttpLib2Error(content)
371
372 # status >= 500 means transient failures.
373 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700374 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375 try_count += 1
376 assert False, 'unreachable'
377
378
qyearsley1fdfcb62016-10-24 13:22:03 -0700379def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700380 """Returns a dict mapping bucket names to builders and tests,
381 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700382 """
qyearsleydd49f942016-10-28 11:57:22 -0700383 # If no bots are listed, we try to get a set of builders and tests based
384 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700385 if not options.bot:
386 change = changelist.GetChange(
387 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700388 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700389 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700390 change=change,
391 changed_files=change.LocalPaths(),
392 repository_root=settings.GetRoot(),
393 default_presubmit=None,
394 project=None,
395 verbose=options.verbose,
396 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700397 if masters is None:
398 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100399 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700400
qyearsley1fdfcb62016-10-24 13:22:03 -0700401 if options.bucket:
402 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700403 if options.master:
404 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700405
qyearsleydd49f942016-10-28 11:57:22 -0700406 # If bots are listed but no master or bucket, then we need to find out
407 # the corresponding master for each bot.
408 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
409 if error_message:
410 option_parser.error(
411 'Tryserver master cannot be found because: %s\n'
412 'Please manually specify the tryserver master, e.g. '
413 '"-m tryserver.chromium.linux".' % error_message)
414 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700415
416
qyearsley123a4682016-10-26 09:12:17 -0700417def _get_bucket_map_for_builders(builders):
418 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 map_url = 'https://builders-map.appspot.com/'
420 try:
qyearsley123a4682016-10-26 09:12:17 -0700421 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 except urllib2.URLError as e:
423 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
424 (map_url, e))
425 except ValueError as e:
426 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700427 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700428 return None, 'Failed to build master map.'
429
qyearsley123a4682016-10-26 09:12:17 -0700430 bucket_map = {}
431 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800432 bucket = builders_map.get(builder, {}).get('bucket')
433 if bucket:
434 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700435 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700436
437
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800438def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700439 """Sends a request to Buildbucket to trigger try jobs for a changelist.
440
441 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700442 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700443 changelist: Changelist that the try jobs are associated with.
444 buckets: A nested dict mapping bucket names to builders to tests.
445 options: Command-line options.
446 """
tandriide281ae2016-10-12 06:02:30 -0700447 assert changelist.GetIssue(), 'CL must be uploaded first'
448 codereview_url = changelist.GetCodereviewServer()
449 assert codereview_url, 'CL must be uploaded first'
450 patchset = patchset or changelist.GetMostRecentPatchset()
451 assert patchset, 'CL must be uploaded first'
452
453 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700454 # Cache the buildbucket credentials under the codereview host key, so that
455 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700456 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000457 http = authenticator.authorize(httplib2.Http())
458 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700459
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000460 buildbucket_put_url = (
461 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000462 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000463 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700464 hostname=codereview_host,
465 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700467
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700468 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800469 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700470 if options.clobber:
471 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700472 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700473 if extra_properties:
474 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000475
476 batch_req_body = {'builds': []}
477 print_text = []
478 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700479 for bucket, builders_and_tests in sorted(buckets.iteritems()):
480 print_text.append('Bucket: %s' % bucket)
481 master = None
482 if bucket.startswith(MASTER_PREFIX):
483 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000484 for builder, tests in sorted(builders_and_tests.iteritems()):
485 print_text.append(' %s: %s' % (builder, tests))
486 parameters = {
487 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000488 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100489 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'revision': options.revision,
491 }],
tandrii8c5a3532016-11-04 07:52:02 -0700492 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000493 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000494 if 'presubmit' in builder.lower():
495 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000496 if tests:
497 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700498
499 tags = [
500 'builder:%s' % builder,
501 'buildset:%s' % buildset,
502 'user_agent:git_cl_try',
503 ]
504 if master:
505 parameters['properties']['master'] = master
506 tags.append('master:%s' % master)
507
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000508 batch_req_body['builds'].append(
509 {
510 'bucket': bucket,
511 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700513 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000514 }
515 )
516
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700518 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 http,
520 buildbucket_put_url,
521 'PUT',
522 body=json.dumps(batch_req_body),
523 headers={'Content-Type': 'application/json'}
524 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000525 print_text.append('To see results here, run: git cl try-results')
526 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700527 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000528
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000529
tandrii221ab252016-10-06 08:12:04 -0700530def fetch_try_jobs(auth_config, changelist, buildbucket_host,
531 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700532 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533
qyearsley53f48a12016-09-01 10:45:13 -0700534 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 """
tandrii221ab252016-10-06 08:12:04 -0700536 assert buildbucket_host
537 assert changelist.GetIssue(), 'CL must be uploaded first'
538 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
539 patchset = patchset or changelist.GetMostRecentPatchset()
540 assert patchset, 'CL must be uploaded first'
541
542 codereview_url = changelist.GetCodereviewServer()
543 codereview_host = urlparse.urlparse(codereview_url).hostname
544 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 if authenticator.has_cached_credentials():
546 http = authenticator.authorize(httplib2.Http())
547 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700548 print('Warning: Some results might be missing because %s' %
549 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700550 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 http = httplib2.Http()
552
553 http.force_exception_to_status_code = True
554
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000555 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700556 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700558 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 params = {'tag': 'buildset:%s' % buildset}
560
561 builds = {}
562 while True:
563 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700564 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700566 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 for build in content.get('builds', []):
568 builds[build['id']] = build
569 if 'next_cursor' in content:
570 params['start_cursor'] = content['next_cursor']
571 else:
572 break
573 return builds
574
575
qyearsleyeab3c042016-08-24 09:18:28 -0700576def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 """Prints nicely result of fetch_try_jobs."""
578 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700579 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 return
581
582 # Make a copy, because we'll be modifying builds dictionary.
583 builds = builds.copy()
584 builder_names_cache = {}
585
586 def get_builder(b):
587 try:
588 return builder_names_cache[b['id']]
589 except KeyError:
590 try:
591 parameters = json.loads(b['parameters_json'])
592 name = parameters['builder_name']
593 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700594 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700595 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000596 name = None
597 builder_names_cache[b['id']] = name
598 return name
599
600 def get_bucket(b):
601 bucket = b['bucket']
602 if bucket.startswith('master.'):
603 return bucket[len('master.'):]
604 return bucket
605
606 if options.print_master:
607 name_fmt = '%%-%ds %%-%ds' % (
608 max(len(str(get_bucket(b))) for b in builds.itervalues()),
609 max(len(str(get_builder(b))) for b in builds.itervalues()))
610 def get_name(b):
611 return name_fmt % (get_bucket(b), get_builder(b))
612 else:
613 name_fmt = '%%-%ds' % (
614 max(len(str(get_builder(b))) for b in builds.itervalues()))
615 def get_name(b):
616 return name_fmt % get_builder(b)
617
618 def sort_key(b):
619 return b['status'], b.get('result'), get_name(b), b.get('url')
620
621 def pop(title, f, color=None, **kwargs):
622 """Pop matching builds from `builds` dict and print them."""
623
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000624 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000625 colorize = str
626 else:
627 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
628
629 result = []
630 for b in builds.values():
631 if all(b.get(k) == v for k, v in kwargs.iteritems()):
632 builds.pop(b['id'])
633 result.append(b)
634 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700635 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000636 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700637 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000638
639 total = len(builds)
640 pop(status='COMPLETED', result='SUCCESS',
641 title='Successes:', color=Fore.GREEN,
642 f=lambda b: (get_name(b), b.get('url')))
643 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
644 title='Infra Failures:', color=Fore.MAGENTA,
645 f=lambda b: (get_name(b), b.get('url')))
646 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
647 title='Failures:', color=Fore.RED,
648 f=lambda b: (get_name(b), b.get('url')))
649 pop(status='COMPLETED', result='CANCELED',
650 title='Canceled:', color=Fore.MAGENTA,
651 f=lambda b: (get_name(b),))
652 pop(status='COMPLETED', result='FAILURE',
653 failure_reason='INVALID_BUILD_DEFINITION',
654 title='Wrong master/builder name:', color=Fore.MAGENTA,
655 f=lambda b: (get_name(b),))
656 pop(status='COMPLETED', result='FAILURE',
657 title='Other failures:',
658 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
659 pop(status='COMPLETED',
660 title='Other finished:',
661 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
662 pop(status='STARTED',
663 title='Started:', color=Fore.YELLOW,
664 f=lambda b: (get_name(b), b.get('url')))
665 pop(status='SCHEDULED',
666 title='Scheduled:',
667 f=lambda b: (get_name(b), 'id=%s' % b['id']))
668 # The last section is just in case buildbucket API changes OR there is a bug.
669 pop(title='Other:',
670 f=lambda b: (get_name(b), 'id=%s' % b['id']))
671 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700672 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000673
674
Aiden Bennerc08566e2018-10-03 17:52:42 +0000675def _ComputeDiffLineRanges(files, upstream_commit):
676 """Gets the changed line ranges for each file since upstream_commit.
677
678 Parses a git diff on provided files and returns a dict that maps a file name
679 to an ordered list of range tuples in the form (start_line, count).
680 Ranges are in the same format as a git diff.
681 """
682 # If files is empty then diff_output will be a full diff.
683 if len(files) == 0:
684 return {}
685
686 # Take diff and find the line ranges where there are changes.
687 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
688 diff_output = RunGit(diff_cmd)
689
690 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
691 # 2 capture groups
692 # 0 == fname of diff file
693 # 1 == 'diff_start,diff_count' or 'diff_start'
694 # will match each of
695 # diff --git a/foo.foo b/foo.py
696 # @@ -12,2 +14,3 @@
697 # @@ -12,2 +17 @@
698 # running re.findall on the above string with pattern will give
699 # [('foo.py', ''), ('', '14,3'), ('', '17')]
700
701 curr_file = None
702 line_diffs = {}
703 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
704 if match[0] != '':
705 # Will match the second filename in diff --git a/a.py b/b.py.
706 curr_file = match[0]
707 line_diffs[curr_file] = []
708 else:
709 # Matches +14,3
710 if ',' in match[1]:
711 diff_start, diff_count = match[1].split(',')
712 else:
713 # Single line changes are of the form +12 instead of +12,1.
714 diff_start = match[1]
715 diff_count = 1
716
717 diff_start = int(diff_start)
718 diff_count = int(diff_count)
719
720 # If diff_count == 0 this is a removal we can ignore.
721 line_diffs[curr_file].append((diff_start, diff_count))
722
723 return line_diffs
724
725
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000726def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000727 """Checks if a yapf file is in any parent directory of fpath until top_dir.
728
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000729 Recursively checks parent directories to find yapf file and if no yapf file
730 is found returns None. Uses yapf_config_cache as a cache for
731 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000732 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000733 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000734 # Return result if we've already computed it.
735 if fpath in yapf_config_cache:
736 return yapf_config_cache[fpath]
737
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000738 parent_dir = os.path.dirname(fpath)
739 if os.path.isfile(fpath):
740 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000741 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000742 # Otherwise fpath is a directory
743 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
744 if os.path.isfile(yapf_file):
745 ret = yapf_file
746 elif fpath == top_dir or parent_dir == fpath:
747 # If we're at the top level directory, or if we're at root
748 # there is no provided style.
749 ret = None
750 else:
751 # Otherwise recurse on the current directory.
752 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000753 yapf_config_cache[fpath] = ret
754 return ret
755
756
qyearsley53f48a12016-09-01 10:45:13 -0700757def write_try_results_json(output_file, builds):
758 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
759
760 The input |builds| dict is assumed to be generated by Buildbucket.
761 Buildbucket documentation: http://goo.gl/G0s101
762 """
763
764 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800765 """Extracts some of the information from one build dict."""
766 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700767 return {
768 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700769 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800770 'builder_name': parameters.get('builder_name'),
771 'created_ts': build.get('created_ts'),
772 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700773 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800774 'result': build.get('result'),
775 'status': build.get('status'),
776 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700777 'url': build.get('url'),
778 }
779
780 converted = []
781 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000782 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700783 write_json(output_file, converted)
784
785
Aaron Gable13101a62018-02-09 13:20:41 -0800786def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000787 """Prints statistics about the change to the user."""
788 # --no-ext-diff is broken in some versions of Git, so try to work around
789 # this by overriding the environment (but there is still a problem if the
790 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000792 if 'GIT_EXTERNAL_DIFF' in env:
793 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000794
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000795 try:
796 stdout = sys.stdout.fileno()
797 except AttributeError:
798 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000799 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800800 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000801 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000802
803
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000804class BuildbucketResponseException(Exception):
805 pass
806
807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808class Settings(object):
809 def __init__(self):
810 self.default_server = None
811 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000812 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813 self.tree_status_url = None
814 self.viewvc_url = None
815 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000817 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000818 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000819 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000820 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000821 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
823 def LazyUpdateIfNeeded(self):
824 """Updates the settings from a codereview.settings file, if available."""
825 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000826 # The only value that actually changes the behavior is
827 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000828 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000829 error_ok=True
830 ).strip().lower()
831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000833 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 LoadCodereviewSettingsFromFile(cr_settings_file)
835 self.updated = True
836
837 def GetDefaultServerUrl(self, error_ok=False):
838 if not self.default_server:
839 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000840 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000841 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 if error_ok:
843 return self.default_server
844 if not self.default_server:
845 error_message = ('Could not find settings file. You must configure '
846 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000847 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000848 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.default_server
850
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000851 @staticmethod
852 def GetRelativeRoot():
853 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000856 if self.root is None:
857 self.root = os.path.abspath(self.GetRelativeRoot())
858 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000860 def GetGitMirror(self, remote='origin'):
861 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000862 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000863 if not os.path.isdir(local_url):
864 return None
865 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
866 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100867 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100868 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000869 if mirror.exists():
870 return mirror
871 return None
872
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 def GetTreeStatusUrl(self, error_ok=False):
874 if not self.tree_status_url:
875 error_message = ('You must configure your tree status URL by running '
876 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000877 self.tree_status_url = self._GetRietveldConfig(
878 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 return self.tree_status_url
880
881 def GetViewVCUrl(self):
882 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000883 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 return self.viewvc_url
885
rmistry@google.com90752582014-01-14 21:04:50 +0000886 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000887 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000888
rmistry@google.com78948ed2015-07-08 23:09:57 +0000889 def GetIsSkipDependencyUpload(self, branch_name):
890 """Returns true if specified branch should skip dep uploads."""
891 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
892 error_ok=True)
893
rmistry@google.com5626a922015-02-26 14:03:30 +0000894 def GetRunPostUploadHook(self):
895 run_post_upload_hook = self._GetRietveldConfig(
896 'run-post-upload-hook', error_ok=True)
897 return run_post_upload_hook == "True"
898
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000899 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000900 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000901
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000902 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000903 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000904
ukai@chromium.orge8077812012-02-03 03:41:46 +0000905 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700906 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000907 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700908 self.is_gerrit = (
909 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000910 return self.is_gerrit
911
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000912 def GetSquashGerritUploads(self):
913 """Return true if uploads to Gerrit should be squashed by default."""
914 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700915 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
916 if self.squash_gerrit_uploads is None:
917 # Default is squash now (http://crbug.com/611892#c23).
918 self.squash_gerrit_uploads = not (
919 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
920 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000921 return self.squash_gerrit_uploads
922
tandriia60502f2016-06-20 02:01:53 -0700923 def GetSquashGerritUploadsOverride(self):
924 """Return True or False if codereview.settings should be overridden.
925
926 Returns None if no override has been defined.
927 """
928 # See also http://crbug.com/611892#c23
929 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
930 error_ok=True).strip()
931 if result == 'true':
932 return True
933 if result == 'false':
934 return False
935 return None
936
tandrii@chromium.org28253532016-04-14 13:46:56 +0000937 def GetGerritSkipEnsureAuthenticated(self):
938 """Return True if EnsureAuthenticated should not be done for Gerrit
939 uploads."""
940 if self.gerrit_skip_ensure_authenticated is None:
941 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000942 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000943 error_ok=True).strip() == 'true')
944 return self.gerrit_skip_ensure_authenticated
945
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000946 def GetGitEditor(self):
947 """Return the editor specified in the git config, or None if none is."""
948 if self.git_editor is None:
949 self.git_editor = self._GetConfig('core.editor', error_ok=True)
950 return self.git_editor or None
951
thestig@chromium.org44202a22014-03-11 19:22:18 +0000952 def GetLintRegex(self):
953 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
954 DEFAULT_LINT_REGEX)
955
956 def GetLintIgnoreRegex(self):
957 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
958 DEFAULT_LINT_IGNORE_REGEX)
959
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000960 def GetProject(self):
961 if not self.project:
962 self.project = self._GetRietveldConfig('project', error_ok=True)
963 return self.project
964
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000965 def _GetRietveldConfig(self, param, **kwargs):
966 return self._GetConfig('rietveld.' + param, **kwargs)
967
rmistry@google.com78948ed2015-07-08 23:09:57 +0000968 def _GetBranchConfig(self, branch_name, param, **kwargs):
969 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
970
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971 def _GetConfig(self, param, **kwargs):
972 self.LazyUpdateIfNeeded()
973 return RunGit(['config', param], **kwargs).strip()
974
975
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100976@contextlib.contextmanager
977def _get_gerrit_project_config_file(remote_url):
978 """Context manager to fetch and store Gerrit's project.config from
979 refs/meta/config branch and store it in temp file.
980
981 Provides a temporary filename or None if there was error.
982 """
983 error, _ = RunGitWithCode([
984 'fetch', remote_url,
985 '+refs/meta/config:refs/git_cl/meta/config'])
986 if error:
987 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700988 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100989 (remote_url, error))
990 yield None
991 return
992
993 error, project_config_data = RunGitWithCode(
994 ['show', 'refs/git_cl/meta/config:project.config'])
995 if error:
996 print('WARNING: project.config file not found')
997 yield None
998 return
999
1000 with gclient_utils.temporary_directory() as tempdir:
1001 project_config_file = os.path.join(tempdir, 'project.config')
1002 gclient_utils.FileWrite(project_config_file, project_config_data)
1003 yield project_config_file
1004
1005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006def ShortBranchName(branch):
1007 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001008 return branch.replace('refs/heads/', '', 1)
1009
1010
1011def GetCurrentBranchRef():
1012 """Returns branch ref (e.g., refs/heads/master) or None."""
1013 return RunGit(['symbolic-ref', 'HEAD'],
1014 stderr=subprocess2.VOID, error_ok=True).strip() or None
1015
1016
1017def GetCurrentBranch():
1018 """Returns current branch or None.
1019
1020 For refs/heads/* branches, returns just last part. For others, full ref.
1021 """
1022 branchref = GetCurrentBranchRef()
1023 if branchref:
1024 return ShortBranchName(branchref)
1025 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026
1027
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001028class _CQState(object):
1029 """Enum for states of CL with respect to Commit Queue."""
1030 NONE = 'none'
1031 DRY_RUN = 'dry_run'
1032 COMMIT = 'commit'
1033
1034 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1035
1036
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001037class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001038 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001039 self.issue = issue
1040 self.patchset = patchset
1041 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001042 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001043 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001044
1045 @property
1046 def valid(self):
1047 return self.issue is not None
1048
1049
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001050def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001051 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1052 fail_result = _ParsedIssueNumberArgument()
1053
1054 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001055 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001056 if not arg.startswith('http'):
1057 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001058
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001059 url = gclient_utils.UpgradeToHttps(arg)
1060 try:
1061 parsed_url = urlparse.urlparse(url)
1062 except ValueError:
1063 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001064
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001065 if codereview is not None:
1066 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1067 return parsed or fail_result
1068
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001069 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001070
1071
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001072def _create_description_from_log(args):
1073 """Pulls out the commit log to use as a base for the CL description."""
1074 log_args = []
1075 if len(args) == 1 and not args[0].endswith('.'):
1076 log_args = [args[0] + '..']
1077 elif len(args) == 1 and args[0].endswith('...'):
1078 log_args = [args[0][:-1]]
1079 elif len(args) == 2:
1080 log_args = [args[0] + '..' + args[1]]
1081 else:
1082 log_args = args[:] # Hope for the best!
1083 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1084
1085
Aaron Gablea45ee112016-11-22 15:14:38 -08001086class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001087 def __init__(self, issue, url):
1088 self.issue = issue
1089 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001090 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001091
1092 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001093 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001094 self.issue, self.url)
1095
1096
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001097_CommentSummary = collections.namedtuple(
1098 '_CommentSummary', ['date', 'message', 'sender',
1099 # TODO(tandrii): these two aren't known in Gerrit.
1100 'approval', 'disapproval'])
1101
1102
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 """Changelist works with one changelist in local branch.
1105
1106 Supports two codereview backends: Rietveld or Gerrit, selected at object
1107 creation.
1108
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001109 Notes:
1110 * Not safe for concurrent multi-{thread,process} use.
1111 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001112 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001113 """
1114
1115 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1116 """Create a new ChangeList instance.
1117
1118 If issue is given, the codereview must be given too.
1119
1120 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1121 Otherwise, it's decided based on current configuration of the local branch,
1122 with default being 'rietveld' for backwards compatibility.
1123 See _load_codereview_impl for more details.
1124
1125 **kwargs will be passed directly to codereview implementation.
1126 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001128 global settings
1129 if not settings:
1130 # Happens when git_cl.py is used as a utility library.
1131 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001132
1133 if issue:
1134 assert codereview, 'codereview must be known, if issue is known'
1135
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 self.branchref = branchref
1137 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001138 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.branch = ShortBranchName(self.branchref)
1140 else:
1141 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001143 self.lookedup_issue = False
1144 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 self.has_description = False
1146 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001149 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001150 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001151 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001152 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 assert self._codereview_impl
1158 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001159
1160 def _load_codereview_impl(self, codereview=None, **kwargs):
1161 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001162 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1163 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1164 self._codereview = codereview
1165 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 return
1167
1168 # Automatic selection based on issue number set for a current branch.
1169 # Rietveld takes precedence over Gerrit.
1170 assert not self.issue
1171 # Whether we find issue or not, we are doing the lookup.
1172 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001173 if self.GetBranch():
1174 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1175 issue = _git_get_branch_config_value(
1176 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1177 if issue:
1178 self._codereview = codereview
1179 self._codereview_impl = cls(self, **kwargs)
1180 self.issue = int(issue)
1181 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182
1183 # No issue is set for this branch, so decide based on repo-wide settings.
1184 return self._load_codereview_impl(
1185 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1186 **kwargs)
1187
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001188 def IsGerrit(self):
1189 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001190
1191 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001192 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001193
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001194 The return value is a string suitable for passing to git cl with the --cc
1195 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 """
1197 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001198 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001199 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001200 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1201 return self.cc
1202
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001203 def GetCCListWithoutDefault(self):
1204 """Return the users cc'd on this CL excluding default ones."""
1205 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001206 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001207 return self.cc
1208
Daniel Cheng7227d212017-11-17 08:12:37 -08001209 def ExtendCC(self, more_cc):
1210 """Extends the list of users to cc on this CL based on the changed files."""
1211 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212
1213 def GetBranch(self):
1214 """Returns the short branch name, e.g. 'master'."""
1215 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001217 if not branchref:
1218 return None
1219 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.branch = ShortBranchName(self.branchref)
1221 return self.branch
1222
1223 def GetBranchRef(self):
1224 """Returns the full branch name, e.g. 'refs/heads/master'."""
1225 self.GetBranch() # Poke the lazy loader.
1226 return self.branchref
1227
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001228 def ClearBranch(self):
1229 """Clears cached branch data of this object."""
1230 self.branch = self.branchref = None
1231
tandrii5d48c322016-08-18 16:19:37 -07001232 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1233 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1234 kwargs['branch'] = self.GetBranch()
1235 return _git_get_branch_config_value(key, default, **kwargs)
1236
1237 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1238 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1239 assert self.GetBranch(), (
1240 'this CL must have an associated branch to %sset %s%s' %
1241 ('un' if value is None else '',
1242 key,
1243 '' if value is None else ' to %r' % value))
1244 kwargs['branch'] = self.GetBranch()
1245 return _git_set_branch_config_value(key, value, **kwargs)
1246
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001247 @staticmethod
1248 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001249 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 e.g. 'origin', 'refs/heads/master'
1251 """
1252 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001253 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001256 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001258 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1259 error_ok=True).strip()
1260 if upstream_branch:
1261 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001263 # Else, try to guess the origin remote.
1264 remote_branches = RunGit(['branch', '-r']).split()
1265 if 'origin/master' in remote_branches:
1266 # Fall back on origin/master if it exits.
1267 remote = 'origin'
1268 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001270 DieWithError(
1271 'Unable to determine default branch to diff against.\n'
1272 'Either pass complete "git diff"-style arguments, like\n'
1273 ' git cl upload origin/master\n'
1274 'or verify this branch is set up to track another \n'
1275 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276
1277 return remote, upstream_branch
1278
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001279 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001280 upstream_branch = self.GetUpstreamBranch()
1281 if not BranchExists(upstream_branch):
1282 DieWithError('The upstream for the current branch (%s) does not exist '
1283 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001284 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001285 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 def GetUpstreamBranch(self):
1288 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001289 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001291 upstream_branch = upstream_branch.replace('refs/heads/',
1292 'refs/remotes/%s/' % remote)
1293 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1294 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 self.upstream_branch = upstream_branch
1296 return self.upstream_branch
1297
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001299 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, branch = None, self.GetBranch()
1301 seen_branches = set()
1302 while branch not in seen_branches:
1303 seen_branches.add(branch)
1304 remote, branch = self.FetchUpstreamTuple(branch)
1305 branch = ShortBranchName(branch)
1306 if remote != '.' or branch.startswith('refs/remotes'):
1307 break
1308 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001309 remotes = RunGit(['remote'], error_ok=True).split()
1310 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001314 logging.warn('Could not determine which remote this change is '
1315 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001316 else:
1317 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001318 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001319 branch = 'HEAD'
1320 if branch.startswith('refs/remotes'):
1321 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001322 elif branch.startswith('refs/branch-heads/'):
1323 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 else:
1325 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001326 return self._remote
1327
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 def GitSanityChecks(self, upstream_git_obj):
1329 """Checks git repo status and ensures diff is from local commits."""
1330
sbc@chromium.org79706062015-01-14 21:18:12 +00001331 if upstream_git_obj is None:
1332 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001333 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001334 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001336 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001337 return False
1338
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 # Verify the commit we're diffing against is in our current branch.
1340 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1341 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1342 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001343 print('ERROR: %s is not in the current branch. You may need to rebase '
1344 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 return False
1346
1347 # List the commits inside the diff, and verify they are all local.
1348 commits_in_diff = RunGit(
1349 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1350 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1351 remote_branch = remote_branch.strip()
1352 if code != 0:
1353 _, remote_branch = self.GetRemoteBranch()
1354
1355 commits_in_remote = RunGit(
1356 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1357
1358 common_commits = set(commits_in_diff) & set(commits_in_remote)
1359 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001360 print('ERROR: Your diff contains %d commits already in %s.\n'
1361 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1362 'the diff. If you are using a custom git flow, you can override'
1363 ' the reference used for this check with "git config '
1364 'gitcl.remotebranch <git-ref>".' % (
1365 len(common_commits), remote_branch, upstream_git_obj),
1366 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001367 return False
1368 return True
1369
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001370 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001371 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001372
1373 Returns None if it is not set.
1374 """
tandrii5d48c322016-08-18 16:19:37 -07001375 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 def GetRemoteUrl(self):
1378 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1379
1380 Returns None if there is no remote.
1381 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001382 is_cached, value = self._cached_remote_url
1383 if is_cached:
1384 return value
1385
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001386 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001387 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1388
1389 # If URL is pointing to a local directory, it is probably a git cache.
1390 if os.path.isdir(url):
1391 url = RunGit(['config', 'remote.%s.url' % remote],
1392 error_ok=True,
1393 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001394 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001395 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001397 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001398 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001399 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001400 self.issue = self._GitGetBranchConfigValue(
1401 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001402 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 return self.issue
1404
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 def GetIssueURL(self):
1406 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001407 issue = self.GetIssue()
1408 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001409 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001410 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001412 def GetDescription(self, pretty=False, force=False):
1413 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001415 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 self.has_description = True
1417 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001418 # Set width to 72 columns + 2 space indent.
1419 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001421 lines = self.description.splitlines()
1422 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return self.description
1424
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001425 def GetDescriptionFooters(self):
1426 """Returns (non_footer_lines, footers) for the commit message.
1427
1428 Returns:
1429 non_footer_lines (list(str)) - Simple list of description lines without
1430 any footer. The lines do not contain newlines, nor does the list contain
1431 the empty line between the message and the footers.
1432 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1433 [("Change-Id", "Ideadbeef...."), ...]
1434 """
1435 raw_description = self.GetDescription()
1436 msg_lines, _, footers = git_footers.split_footers(raw_description)
1437 if footers:
1438 msg_lines = msg_lines[:len(msg_lines)-1]
1439 return msg_lines, footers
1440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001442 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001444 self.patchset = self._GitGetBranchConfigValue(
1445 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001446 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 return self.patchset
1448
1449 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001450 """Set this branch's patchset. If patchset=0, clears the patchset."""
1451 assert self.GetBranch()
1452 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001453 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001454 else:
1455 self.patchset = int(patchset)
1456 self._GitSetBranchConfigValue(
1457 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001459 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001460 """Set this branch's issue. If issue isn't given, clears the issue."""
1461 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001463 issue = int(issue)
1464 self._GitSetBranchConfigValue(
1465 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001466 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467 codereview_server = self._codereview_impl.GetCodereviewServer()
1468 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001469 self._GitSetBranchConfigValue(
1470 self._codereview_impl.CodereviewServerConfigKey(),
1471 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 else:
tandrii5d48c322016-08-18 16:19:37 -07001473 # Reset all of these just to be clean.
1474 reset_suffixes = [
1475 'last-upload-hash',
1476 self._codereview_impl.IssueConfigKey(),
1477 self._codereview_impl.PatchsetConfigKey(),
1478 self._codereview_impl.CodereviewServerConfigKey(),
1479 ] + self._PostUnsetIssueProperties()
1480 for prop in reset_suffixes:
1481 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001482 msg = RunGit(['log', '-1', '--format=%B']).strip()
1483 if msg and git_footers.get_footer_change_id(msg):
1484 print('WARNING: The change patched into this branch has a Change-Id. '
1485 'Removing it.')
1486 RunGit(['commit', '--amend', '-m',
1487 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001488 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001489 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490
dnjba1b0f32016-09-02 12:37:42 -07001491 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001492 if not self.GitSanityChecks(upstream_branch):
1493 DieWithError('\nGit sanity check failure')
1494
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001495 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001496 if not root:
1497 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001498 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001499
1500 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001501 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001502 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001504 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001505 except subprocess2.CalledProcessError:
1506 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001507 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001508 'This branch probably doesn\'t exist anymore. To reset the\n'
1509 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001510 ' git branch --set-upstream-to origin/master %s\n'
1511 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001512 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001513
maruel@chromium.org52424302012-08-29 15:14:30 +00001514 issue = self.GetIssue()
1515 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001516 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001517 description = self.GetDescription()
1518 else:
1519 # If the change was never uploaded, use the log messages of all commits
1520 # up to the branch point, as git cl upload will prefill the description
1521 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001522 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1523 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001524
1525 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001526 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001527 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001528 name,
1529 description,
1530 absroot,
1531 files,
1532 issue,
1533 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001534 author,
1535 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001536
dsansomee2d6fd92016-09-08 00:10:47 -07001537 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001538 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001539 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001540 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001541
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001542 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1543 """Sets the description for this CL remotely.
1544
1545 You can get description_lines and footers with GetDescriptionFooters.
1546
1547 Args:
1548 description_lines (list(str)) - List of CL description lines without
1549 newline characters.
1550 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1551 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1552 `List-Of-Tokens`). It will be case-normalized so that each token is
1553 title-cased.
1554 """
1555 new_description = '\n'.join(description_lines)
1556 if footers:
1557 new_description += '\n'
1558 for k, v in footers:
1559 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1560 if not git_footers.FOOTER_PATTERN.match(foot):
1561 raise ValueError('Invalid footer %r' % foot)
1562 new_description += foot + '\n'
1563 self.UpdateDescription(new_description, force)
1564
Edward Lesmes8e282792018-04-03 18:50:29 -04001565 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001566 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1567 try:
1568 return presubmit_support.DoPresubmitChecks(change, committing,
1569 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1570 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001571 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1572 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001573 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001574 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001576 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1577 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001578 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1579 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001580 else:
1581 # Assume url.
1582 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1583 urlparse.urlparse(issue_arg))
1584 if not parsed_issue_arg or not parsed_issue_arg.valid:
1585 DieWithError('Failed to parse issue argument "%s". '
1586 'Must be an issue number or a valid URL.' % issue_arg)
1587 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001588 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001589
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 def CMDUpload(self, options, git_diff_args, orig_args):
1591 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001592 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001593 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001595 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596 else:
1597 if self.GetBranch() is None:
1598 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1599
1600 # Default to diffing against common ancestor of upstream branch
1601 base_branch = self.GetCommonAncestorWithUpstream()
1602 git_diff_args = [base_branch, 'HEAD']
1603
Aaron Gablec4c40d12017-05-22 11:49:53 -07001604
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001605 # Fast best-effort checks to abort before running potentially
1606 # expensive hooks if uploading is likely to fail anyway. Passing these
1607 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001608 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001609 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610
1611 # Apply watchlists on upload.
1612 change = self.GetChange(base_branch, None)
1613 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1614 files = [f.LocalPath() for f in change.AffectedFiles()]
1615 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001616 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617
1618 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001619 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 # Set the reviewer list now so that presubmit checks can access it.
1621 change_description = ChangeDescription(change.FullDescriptionText())
1622 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001623 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001624 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625 change)
1626 change.SetDescriptionText(change_description.description)
1627 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001628 may_prompt=not options.force,
1629 verbose=options.verbose,
1630 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631 if not hook_results.should_continue():
1632 return 1
1633 if not options.reviewers and hook_results.reviewers:
1634 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001635 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001636
Aaron Gable13101a62018-02-09 13:20:41 -08001637 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001638 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001639 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001640 _git_set_branch_config_value('last-upload-hash',
1641 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 # Run post upload hooks, if specified.
1643 if settings.GetRunPostUploadHook():
1644 presubmit_support.DoPostUploadExecuter(
1645 change,
1646 self,
1647 settings.GetRoot(),
1648 options.verbose,
1649 sys.stdout)
1650
1651 # Upload all dependencies if specified.
1652 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001653 print()
1654 print('--dependencies has been specified.')
1655 print('All dependent local branches will be re-uploaded.')
1656 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001657 # Remove the dependencies flag from args so that we do not end up in a
1658 # loop.
1659 orig_args.remove('--dependencies')
1660 ret = upload_branch_deps(self, orig_args)
1661 return ret
1662
Ravi Mistry31e7d562018-04-02 12:53:57 -04001663 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1664 """Sets labels on the change based on the provided flags.
1665
1666 Sets labels if issue is already uploaded and known, else returns without
1667 doing anything.
1668
1669 Args:
1670 enable_auto_submit: Sets Auto-Submit+1 on the change.
1671 use_commit_queue: Sets Commit-Queue+2 on the change.
1672 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1673 both use_commit_queue and cq_dry_run are true.
1674 """
1675 if not self.GetIssue():
1676 return
1677 try:
1678 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1679 cq_dry_run)
1680 return 0
1681 except KeyboardInterrupt:
1682 raise
1683 except:
1684 labels = []
1685 if enable_auto_submit:
1686 labels.append('Auto-Submit')
1687 if use_commit_queue or cq_dry_run:
1688 labels.append('Commit-Queue')
1689 print('WARNING: Failed to set label(s) on your change: %s\n'
1690 'Either:\n'
1691 ' * Your project does not have the above label(s),\n'
1692 ' * You don\'t have permission to set the above label(s),\n'
1693 ' * There\'s a bug in this code (see stack trace below).\n' %
1694 (', '.join(labels)))
1695 # Still raise exception so that stack trace is printed.
1696 raise
1697
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001698 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001699 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001700
1701 Issue must have been already uploaded and known.
1702 """
1703 assert new_state in _CQState.ALL_STATES
1704 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001705 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001706 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001707 return 0
1708 except KeyboardInterrupt:
1709 raise
1710 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001711 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001712 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001713 ' * Your project has no CQ,\n'
1714 ' * You don\'t have permission to change the CQ state,\n'
1715 ' * There\'s a bug in this code (see stack trace below).\n'
1716 'Consider specifying which bots to trigger manually or asking your '
1717 'project owners for permissions or contacting Chrome Infra at:\n'
1718 'https://www.chromium.org/infra\n\n' %
1719 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001720 # Still raise exception so that stack trace is printed.
1721 raise
1722
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 # Forward methods to codereview specific implementation.
1724
Aaron Gable636b13f2017-07-14 10:42:48 -07001725 def AddComment(self, message, publish=None):
1726 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001727
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001728 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001729 """Returns list of _CommentSummary for each comment.
1730
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001731 args:
1732 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001733 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001734 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001735
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 def CloseIssue(self):
1737 return self._codereview_impl.CloseIssue()
1738
1739 def GetStatus(self):
1740 return self._codereview_impl.GetStatus()
1741
1742 def GetCodereviewServer(self):
1743 return self._codereview_impl.GetCodereviewServer()
1744
tandriide281ae2016-10-12 06:02:30 -07001745 def GetIssueOwner(self):
1746 """Get owner from codereview, which may differ from this checkout."""
1747 return self._codereview_impl.GetIssueOwner()
1748
Edward Lemur707d70b2018-02-07 00:50:14 +01001749 def GetReviewers(self):
1750 return self._codereview_impl.GetReviewers()
1751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def GetMostRecentPatchset(self):
1753 return self._codereview_impl.GetMostRecentPatchset()
1754
tandriide281ae2016-10-12 06:02:30 -07001755 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001756 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001757 return self._codereview_impl.CannotTriggerTryJobReason()
1758
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001759 def GetTryJobProperties(self, patchset=None):
1760 """Returns dictionary of properties to launch try job."""
1761 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001762
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763 def __getattr__(self, attr):
1764 # This is because lots of untested code accesses Rietveld-specific stuff
1765 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001766 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001767 # Note that child method defines __getattr__ as well, and forwards it here,
1768 # because _RietveldChangelistImpl is not cleaned up yet, and given
1769 # deprecation of Rietveld, it should probably be just removed.
1770 # Until that time, avoid infinite recursion by bypassing __getattr__
1771 # of implementation class.
1772 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001773
1774
1775class _ChangelistCodereviewBase(object):
1776 """Abstract base class encapsulating codereview specifics of a changelist."""
1777 def __init__(self, changelist):
1778 self._changelist = changelist # instance of Changelist
1779
1780 def __getattr__(self, attr):
1781 # Forward methods to changelist.
1782 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1783 # _RietveldChangelistImpl to avoid this hack?
1784 return getattr(self._changelist, attr)
1785
1786 def GetStatus(self):
1787 """Apply a rough heuristic to give a simple summary of an issue's review
1788 or CQ status, assuming adherence to a common workflow.
1789
1790 Returns None if no issue for this branch, or specific string keywords.
1791 """
1792 raise NotImplementedError()
1793
1794 def GetCodereviewServer(self):
1795 """Returns server URL without end slash, like "https://codereview.com"."""
1796 raise NotImplementedError()
1797
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001798 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 """Fetches and returns description from the codereview server."""
1800 raise NotImplementedError()
1801
tandrii5d48c322016-08-18 16:19:37 -07001802 @classmethod
1803 def IssueConfigKey(cls):
1804 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 raise NotImplementedError()
1806
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001807 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001808 def PatchsetConfigKey(cls):
1809 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001810 raise NotImplementedError()
1811
tandrii5d48c322016-08-18 16:19:37 -07001812 @classmethod
1813 def CodereviewServerConfigKey(cls):
1814 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 raise NotImplementedError()
1816
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001817 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001818 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001819 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001820
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001821 def GetGerritObjForPresubmit(self):
1822 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1823 return None
1824
dsansomee2d6fd92016-09-08 00:10:47 -07001825 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001826 """Update the description on codereview site."""
1827 raise NotImplementedError()
1828
Aaron Gable636b13f2017-07-14 10:42:48 -07001829 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001830 """Posts a comment to the codereview site."""
1831 raise NotImplementedError()
1832
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001833 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001834 raise NotImplementedError()
1835
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836 def CloseIssue(self):
1837 """Closes the issue."""
1838 raise NotImplementedError()
1839
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840 def GetMostRecentPatchset(self):
1841 """Returns the most recent patchset number from the codereview site."""
1842 raise NotImplementedError()
1843
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001844 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001845 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001846 """Fetches and applies the issue.
1847
1848 Arguments:
1849 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1850 reject: if True, reject the failed patch instead of switching to 3-way
1851 merge. Rietveld only.
1852 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1853 only.
1854 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001855 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001856 """
1857 raise NotImplementedError()
1858
1859 @staticmethod
1860 def ParseIssueURL(parsed_url):
1861 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1862 failed."""
1863 raise NotImplementedError()
1864
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001865 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001866 """Best effort check that user is authenticated with codereview server.
1867
1868 Arguments:
1869 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001870 refresh: whether to attempt to refresh credentials. Ignored if not
1871 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001872 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001873 raise NotImplementedError()
1874
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001875 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876 """Best effort check that uploading isn't supposed to fail for predictable
1877 reasons.
1878
1879 This method should raise informative exception if uploading shouldn't
1880 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001881
1882 Arguments:
1883 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001884 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001885 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001886
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001887 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001888 """Uploads a change to codereview."""
1889 raise NotImplementedError()
1890
Ravi Mistry31e7d562018-04-02 12:53:57 -04001891 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1892 """Sets labels on the change based on the provided flags.
1893
1894 Issue must have been already uploaded and known.
1895 """
1896 raise NotImplementedError()
1897
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001898 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001899 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001900
1901 Issue must have been already uploaded and known.
1902 """
1903 raise NotImplementedError()
1904
tandriie113dfd2016-10-11 10:20:12 -07001905 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001906 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001907 raise NotImplementedError()
1908
tandriide281ae2016-10-12 06:02:30 -07001909 def GetIssueOwner(self):
1910 raise NotImplementedError()
1911
Edward Lemur707d70b2018-02-07 00:50:14 +01001912 def GetReviewers(self):
1913 raise NotImplementedError()
1914
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001915 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001916 raise NotImplementedError()
1917
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918
1919class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001920
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001921 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922 super(_RietveldChangelistImpl, self).__init__(changelist)
1923 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001924 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001925 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001927 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001928 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001929 self._props = None
1930 self._rpc_server = None
1931
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 def GetCodereviewServer(self):
1933 if not self._rietveld_server:
1934 # If we're on a branch then get the server potentially associated
1935 # with that branch.
1936 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001937 self._rietveld_server = gclient_utils.UpgradeToHttps(
1938 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939 if not self._rietveld_server:
1940 self._rietveld_server = settings.GetDefaultServerUrl()
1941 return self._rietveld_server
1942
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001943 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001944 # No checks for Rietveld because we are deprecating Rietveld.
1945 pass
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001946
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001947 def EnsureCanUploadPatchset(self, force):
1948 # No checks for Rietveld because we are deprecating Rietveld.
1949 pass
1950
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001951 def FetchDescription(self, force=False):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001952 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001953
1954 def GetMostRecentPatchset(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001955 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001957 def GetIssueProperties(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001958 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001959
tandriie113dfd2016-10-11 10:20:12 -07001960 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001961 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07001962
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001963 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001964 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07001965
tandriide281ae2016-10-12 06:02:30 -07001966 def GetIssueOwner(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001967 raise NotImplementedError()
tandriide281ae2016-10-12 06:02:30 -07001968
Edward Lemur707d70b2018-02-07 00:50:14 +01001969 def GetReviewers(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001970 raise NotImplementedError()
Edward Lemur707d70b2018-02-07 00:50:14 +01001971
Aaron Gable636b13f2017-07-14 10:42:48 -07001972 def AddComment(self, message, publish=None):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001973 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001974
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001975 def GetCommentsSummary(self, readable=True):
1976 raise NotImplementedError()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001977
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001978 def GetStatus(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001979 print(
1980 'WARNING! Rietveld is no longer supported.\n'
1981 '\n'
1982 'If you have old branches in your checkout, please archive/delete them.\n'
1983 ' $ git cl archive --help\n'
1984 '\n'
1985 'See also PSA https://groups.google.com/a/chromium.org/'
1986 'forum/#!topic/infra-dev/2DIVzM2wseo\n')
1987 return 'rietveld-not-supported'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001988
dsansomee2d6fd92016-09-08 00:10:47 -07001989 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001990 raise NotImplementedError()
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 def CloseIssue(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001993 raise NotImplementedError()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001995 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001996 return self.SetFlags({flag: value})
1997
1998 def SetFlags(self, flags):
1999 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002000 """
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00002001 raise NotImplementedError
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002003 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004 """Returns an upload.RpcServer() to access this review's rietveld instance.
2005 """
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00002006 raise NotImplementedError
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002008 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002009 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002010 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
tandrii5d48c322016-08-18 16:19:37 -07002012 @classmethod
2013 def PatchsetConfigKey(cls):
2014 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015
tandrii5d48c322016-08-18 16:19:37 -07002016 @classmethod
2017 def CodereviewServerConfigKey(cls):
2018 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002019
Ravi Mistry31e7d562018-04-02 12:53:57 -04002020 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00002021 raise NotImplementedError
Ravi Mistry31e7d562018-04-02 12:53:57 -04002022
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002023 def SetCQState(self, new_state):
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00002024 raise NotImplementedError
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002025
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002026 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002027 directory, force):
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00002028 raise NotImplementedError
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002029
2030 @staticmethod
2031 def ParseIssueURL(parsed_url):
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00002032 raise NotImplementedError
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002033
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002034 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002035 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00002036 raise NotImplementedError
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002037
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002038
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002039class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002040 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002041 # auth_config is Rietveld thing, kept here to preserve interface only.
2042 super(_GerritChangelistImpl, self).__init__(changelist)
2043 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002044 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002045 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002046 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002047 # Map from change number (issue) to its detail cache.
2048 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002049
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002050 if codereview_host is not None:
2051 assert not codereview_host.startswith('https://'), codereview_host
2052 self._gerrit_host = codereview_host
2053 self._gerrit_server = 'https://%s' % codereview_host
2054
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002055 def _GetGerritHost(self):
2056 # Lazy load of configs.
2057 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002058 if self._gerrit_host and '.' not in self._gerrit_host:
2059 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2060 # This happens for internal stuff http://crbug.com/614312.
2061 parsed = urlparse.urlparse(self.GetRemoteUrl())
2062 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002063 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002064 ' Your current remote is: %s' % self.GetRemoteUrl())
2065 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2066 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002067 return self._gerrit_host
2068
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002069 def _GetGitHost(self):
2070 """Returns git host to be used when uploading change to Gerrit."""
2071 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2072
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002073 def GetCodereviewServer(self):
2074 if not self._gerrit_server:
2075 # If we're on a branch then get the server potentially associated
2076 # with that branch.
2077 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002078 self._gerrit_server = self._GitGetBranchConfigValue(
2079 self.CodereviewServerConfigKey())
2080 if self._gerrit_server:
2081 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002082 if not self._gerrit_server:
2083 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2084 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002085 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086 parts[0] = parts[0] + '-review'
2087 self._gerrit_host = '.'.join(parts)
2088 self._gerrit_server = 'https://%s' % self._gerrit_host
2089 return self._gerrit_server
2090
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002091 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002092 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002093 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002094 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002095 logging.warn('can\'t detect Gerrit project.')
2096 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002097 project = urlparse.urlparse(remote_url).path.strip('/')
2098 if project.endswith('.git'):
2099 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002100 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2101 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2102 # gitiles/git-over-https protocol. E.g.,
2103 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2104 # as
2105 # https://chromium.googlesource.com/v8/v8
2106 if project.startswith('a/'):
2107 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002108 return project
2109
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002110 def _GerritChangeIdentifier(self):
2111 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2112
2113 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002114 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002115 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002116 project = self._GetGerritProject()
2117 if project:
2118 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2119 # Fall back on still unique, but less efficient change number.
2120 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002121
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002122 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002123 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002124 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002125
tandrii5d48c322016-08-18 16:19:37 -07002126 @classmethod
2127 def PatchsetConfigKey(cls):
2128 return 'gerritpatchset'
2129
2130 @classmethod
2131 def CodereviewServerConfigKey(cls):
2132 return 'gerritserver'
2133
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002134 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002135 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002136 if settings.GetGerritSkipEnsureAuthenticated():
2137 # For projects with unusual authentication schemes.
2138 # See http://crbug.com/603378.
2139 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002140
2141 # Check presence of cookies only if using cookies-based auth method.
2142 cookie_auth = gerrit_util.Authenticator.get()
2143 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002144 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002145
2146 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002147 self.GetCodereviewServer()
2148 git_host = self._GetGitHost()
2149 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002150
2151 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2152 git_auth = cookie_auth.get_auth_header(git_host)
2153 if gerrit_auth and git_auth:
2154 if gerrit_auth == git_auth:
2155 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002156 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002157 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002158 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002159 ' %s\n'
2160 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002161 ' Consider running the following command:\n'
2162 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002163 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002164 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002165 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002166 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002167 cookie_auth.get_new_password_message(git_host)))
2168 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002169 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002170 return
2171 else:
2172 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002173 ([] if gerrit_auth else [self._gerrit_host]) +
2174 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002175 DieWithError('Credentials for the following hosts are required:\n'
2176 ' %s\n'
2177 'These are read from %s (or legacy %s)\n'
2178 '%s' % (
2179 '\n '.join(missing),
2180 cookie_auth.get_gitcookies_path(),
2181 cookie_auth.get_netrc_path(),
2182 cookie_auth.get_new_password_message(git_host)))
2183
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002184 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002185 if not self.GetIssue():
2186 return
2187
2188 # Warm change details cache now to avoid RPCs later, reducing latency for
2189 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002190 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002191 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002192
2193 status = self._GetChangeDetail()['status']
2194 if status in ('MERGED', 'ABANDONED'):
2195 DieWithError('Change %s has been %s, new uploads are not allowed' %
2196 (self.GetIssueURL(),
2197 'submitted' if status == 'MERGED' else 'abandoned'))
2198
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002199 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2200 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2201 # Apparently this check is not very important? Otherwise get_auth_email
2202 # could have been added to other implementations of Authenticator.
2203 cookies_auth = gerrit_util.Authenticator.get()
2204 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002205 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002206
2207 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002208 if self.GetIssueOwner() == cookies_user:
2209 return
2210 logging.debug('change %s owner is %s, cookies user is %s',
2211 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002212 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002213 # so ask what Gerrit thinks of this user.
2214 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2215 if details['email'] == self.GetIssueOwner():
2216 return
2217 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002218 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002219 'as %s.\n'
2220 'Uploading may fail due to lack of permissions.' %
2221 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2222 confirm_or_exit(action='upload')
2223
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002224 def _PostUnsetIssueProperties(self):
2225 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002226 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002227
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002228 def GetGerritObjForPresubmit(self):
2229 return presubmit_support.GerritAccessor(self._GetGerritHost())
2230
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002231 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002232 """Apply a rough heuristic to give a simple summary of an issue's review
2233 or CQ status, assuming adherence to a common workflow.
2234
2235 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002236 * 'error' - error from review tool (including deleted issues)
2237 * 'unsent' - no reviewers added
2238 * 'waiting' - waiting for review
2239 * 'reply' - waiting for uploader to reply to review
2240 * 'lgtm' - Code-Review label has been set
2241 * 'commit' - in the commit queue
2242 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002243 """
2244 if not self.GetIssue():
2245 return None
2246
2247 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002248 data = self._GetChangeDetail([
2249 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002250 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002251 return 'error'
2252
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002253 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002254 return 'closed'
2255
Aaron Gable9ab38c62017-04-06 14:36:33 -07002256 if data['labels'].get('Commit-Queue', {}).get('approved'):
2257 # The section will have an "approved" subsection if anyone has voted
2258 # the maximum value on the label.
2259 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002260
Aaron Gable9ab38c62017-04-06 14:36:33 -07002261 if data['labels'].get('Code-Review', {}).get('approved'):
2262 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002263
2264 if not data.get('reviewers', {}).get('REVIEWER', []):
2265 return 'unsent'
2266
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002267 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002268 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2269 last_message_author = messages.pop().get('author', {})
2270 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002271 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2272 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002273 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002274 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002275 if last_message_author.get('_account_id') == owner:
2276 # Most recent message was by owner.
2277 return 'waiting'
2278 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002279 # Some reply from non-owner.
2280 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002281
2282 # Somehow there are no messages even though there are reviewers.
2283 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002284
2285 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002286 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002287 patchset = data['revisions'][data['current_revision']]['_number']
2288 self.SetPatchset(patchset)
2289 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002291 def FetchDescription(self, force=False):
2292 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2293 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002294 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002295 return data['revisions'][current_rev]['commit']['message'].encode(
2296 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002297
dsansomee2d6fd92016-09-08 00:10:47 -07002298 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002299 if gerrit_util.HasPendingChangeEdit(
2300 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002301 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002302 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002303 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002304 'unpublished edit. Either publish the edit in the Gerrit web UI '
2305 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002306
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002307 gerrit_util.DeletePendingChangeEdit(
2308 self._GetGerritHost(), self._GerritChangeIdentifier())
2309 gerrit_util.SetCommitMessage(
2310 self._GetGerritHost(), self._GerritChangeIdentifier(),
2311 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002312
Aaron Gable636b13f2017-07-14 10:42:48 -07002313 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002314 gerrit_util.SetReview(
2315 self._GetGerritHost(), self._GerritChangeIdentifier(),
2316 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002317
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002318 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002319 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002320 messages = self._GetChangeDetail(
2321 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2322 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002323 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002324
2325 # Build dictionary of file comments for easy access and sorting later.
2326 # {author+date: {path: {patchset: {line: url+message}}}}
2327 comments = collections.defaultdict(
2328 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2329 for path, line_comments in file_comments.iteritems():
2330 for comment in line_comments:
2331 if comment.get('tag', '').startswith('autogenerated'):
2332 continue
2333 key = (comment['author']['email'], comment['updated'])
2334 if comment.get('side', 'REVISION') == 'PARENT':
2335 patchset = 'Base'
2336 else:
2337 patchset = 'PS%d' % comment['patch_set']
2338 line = comment.get('line', 0)
2339 url = ('https://%s/c/%s/%s/%s#%s%s' %
2340 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2341 'b' if comment.get('side') == 'PARENT' else '',
2342 str(line) if line else ''))
2343 comments[key][path][patchset][line] = (url, comment['message'])
2344
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002345 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002346 for msg in messages:
2347 # Don't bother showing autogenerated messages.
2348 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2349 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002350 # Gerrit spits out nanoseconds.
2351 assert len(msg['date'].split('.')[-1]) == 9
2352 date = datetime.datetime.strptime(msg['date'][:-3],
2353 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002354 message = msg['message']
2355 key = (msg['author']['email'], msg['date'])
2356 if key in comments:
2357 message += '\n'
2358 for path, patchsets in sorted(comments.get(key, {}).items()):
2359 if readable:
2360 message += '\n%s' % path
2361 for patchset, lines in sorted(patchsets.items()):
2362 for line, (url, content) in sorted(lines.items()):
2363 if line:
2364 line_str = 'Line %d' % line
2365 path_str = '%s:%d:' % (path, line)
2366 else:
2367 line_str = 'File comment'
2368 path_str = '%s:0:' % path
2369 if readable:
2370 message += '\n %s, %s: %s' % (patchset, line_str, url)
2371 message += '\n %s\n' % content
2372 else:
2373 message += '\n%s ' % path_str
2374 message += '\n%s\n' % content
2375
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002376 summary.append(_CommentSummary(
2377 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002378 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002379 sender=msg['author']['email'],
2380 # These could be inferred from the text messages and correlated with
2381 # Code-Review label maximum, however this is not reliable.
2382 # Leaving as is until the need arises.
2383 approval=False,
2384 disapproval=False,
2385 ))
2386 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002387
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002388 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002389 gerrit_util.AbandonChange(
2390 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002391
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002392 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002393 gerrit_util.SubmitChange(
2394 self._GetGerritHost(), self._GerritChangeIdentifier(),
2395 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002397 def _GetChangeDetail(self, options=None, no_cache=False):
2398 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002399
2400 If fresh data is needed, set no_cache=True which will clear cache and
2401 thus new data will be fetched from Gerrit.
2402 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002403 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002404 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002405
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002406 # Optimization to avoid multiple RPCs:
2407 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2408 'CURRENT_COMMIT' not in options):
2409 options.append('CURRENT_COMMIT')
2410
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002411 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002412 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002413 options = [o.upper() for o in options]
2414
2415 # Check in cache first unless no_cache is True.
2416 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002417 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002418 else:
2419 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002420 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002421 # Assumption: data fetched before with extra options is suitable
2422 # for return for a smaller set of options.
2423 # For example, if we cached data for
2424 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2425 # and request is for options=[CURRENT_REVISION],
2426 # THEN we can return prior cached data.
2427 if options_set.issubset(cached_options_set):
2428 return data
2429
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002430 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002431 data = gerrit_util.GetChangeDetail(
2432 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002433 except gerrit_util.GerritError as e:
2434 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002435 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002436 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002437
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002438 self._detail_cache.setdefault(cache_key, []).append(
2439 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002440 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002441
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002442 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002443 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002444 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002445 data = gerrit_util.GetChangeCommit(
2446 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002447 except gerrit_util.GerritError as e:
2448 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002449 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002450 raise
agable32978d92016-11-01 12:55:02 -07002451 return data
2452
Olivier Robin75ee7252018-04-13 10:02:56 +02002453 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002454 if git_common.is_dirty_git_tree('land'):
2455 return 1
tandriid60367b2016-06-22 05:25:12 -07002456 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2457 if u'Commit-Queue' in detail.get('labels', {}):
2458 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002459 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2460 'which can test and land changes for you. '
2461 'Are you sure you wish to bypass it?\n',
2462 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002463
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002464 differs = True
tandriic4344b52016-08-29 06:04:54 -07002465 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002466 # Note: git diff outputs nothing if there is no diff.
2467 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002468 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002469 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002470 if detail['current_revision'] == last_upload:
2471 differs = False
2472 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002473 print('WARNING: Local branch contents differ from latest uploaded '
2474 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002475 if differs:
2476 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002477 confirm_or_exit(
2478 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2479 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002480 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002481 elif not bypass_hooks:
2482 hook_results = self.RunHook(
2483 committing=True,
2484 may_prompt=not force,
2485 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002486 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2487 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002488 if not hook_results.should_continue():
2489 return 1
2490
2491 self.SubmitIssue(wait_for_merge=True)
2492 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002493 links = self._GetChangeCommit().get('web_links', [])
2494 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002495 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002496 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002497 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002498 return 0
2499
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002500 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002501 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002502 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002503 assert not directory
2504 assert parsed_issue_arg.valid
2505
2506 self._changelist.issue = parsed_issue_arg.issue
2507
2508 if parsed_issue_arg.hostname:
2509 self._gerrit_host = parsed_issue_arg.hostname
2510 self._gerrit_server = 'https://%s' % self._gerrit_host
2511
tandriic2405f52016-10-10 08:13:15 -07002512 try:
2513 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002514 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002515 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002516
2517 if not parsed_issue_arg.patchset:
2518 # Use current revision by default.
2519 revision_info = detail['revisions'][detail['current_revision']]
2520 patchset = int(revision_info['_number'])
2521 else:
2522 patchset = parsed_issue_arg.patchset
2523 for revision_info in detail['revisions'].itervalues():
2524 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2525 break
2526 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002527 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002528 (parsed_issue_arg.patchset, self.GetIssue()))
2529
Aaron Gable697a91b2018-01-19 15:20:15 -08002530 remote_url = self._changelist.GetRemoteUrl()
2531 if remote_url.endswith('.git'):
2532 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002533 remote_url = remote_url.rstrip('/')
2534
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002535 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002536 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002537
2538 if remote_url != fetch_info['url']:
2539 DieWithError('Trying to patch a change from %s but this repo appears '
2540 'to be %s.' % (fetch_info['url'], remote_url))
2541
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002542 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002543
Aaron Gable62619a32017-06-16 08:22:09 -07002544 if force:
2545 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2546 print('Checked out commit for change %i patchset %i locally' %
2547 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002548 elif nocommit:
2549 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2550 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002551 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002552 RunGit(['cherry-pick', 'FETCH_HEAD'])
2553 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002554 (parsed_issue_arg.issue, patchset))
2555 print('Note: this created a local commit which does not have '
2556 'the same hash as the one uploaded for review. This will make '
2557 'uploading changes based on top of this branch difficult.\n'
2558 'If you want to do that, use "git cl patch --force" instead.')
2559
Stefan Zagerd08043c2017-10-12 12:07:02 -07002560 if self.GetBranch():
2561 self.SetIssue(parsed_issue_arg.issue)
2562 self.SetPatchset(patchset)
2563 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2564 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2565 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2566 else:
2567 print('WARNING: You are in detached HEAD state.\n'
2568 'The patch has been applied to your checkout, but you will not be '
2569 'able to upload a new patch set to the gerrit issue.\n'
2570 'Try using the \'-b\' option if you would like to work on a '
2571 'branch and/or upload a new patch set.')
2572
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002573 return 0
2574
2575 @staticmethod
2576 def ParseIssueURL(parsed_url):
2577 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2578 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002579 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2580 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002581 # Short urls like https://domain/<issue_number> can be used, but don't allow
2582 # specifying the patchset (you'd 404), but we allow that here.
2583 if parsed_url.path == '/':
2584 part = parsed_url.fragment
2585 else:
2586 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002587 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002588 if match:
2589 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002590 issue=int(match.group(3)),
2591 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002592 hostname=parsed_url.netloc,
2593 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002594 return None
2595
tandrii16e0b4e2016-06-07 10:34:28 -07002596 def _GerritCommitMsgHookCheck(self, offer_removal):
2597 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2598 if not os.path.exists(hook):
2599 return
2600 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2601 # custom developer made one.
2602 data = gclient_utils.FileRead(hook)
2603 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2604 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002605 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002606 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002607 'and may interfere with it in subtle ways.\n'
2608 'We recommend you remove the commit-msg hook.')
2609 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002610 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002611 gclient_utils.rm_file_or_tree(hook)
2612 print('Gerrit commit-msg hook removed.')
2613 else:
2614 print('OK, will keep Gerrit commit-msg hook in place.')
2615
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002616 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002617 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002618 if options.squash and options.no_squash:
2619 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002620
2621 if not options.squash and not options.no_squash:
2622 # Load default for user, repo, squash=true, in this order.
2623 options.squash = settings.GetSquashGerritUploads()
2624 elif options.no_squash:
2625 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002626
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002627 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002628 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002629
Aaron Gableb56ad332017-01-06 15:24:31 -08002630 # This may be None; default fallback value is determined in logic below.
2631 title = options.title
2632
Dominic Battre7d1c4842017-10-27 09:17:28 +02002633 # Extract bug number from branch name.
2634 bug = options.bug
2635 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2636 if not bug and match:
2637 bug = match.group(1)
2638
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002639 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002640 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002641 if self.GetIssue():
2642 # Try to get the message from a previous upload.
2643 message = self.GetDescription()
2644 if not message:
2645 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002646 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002648 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002649 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002650 # When uploading a subsequent patchset, -m|--message is taken
2651 # as the patchset title if --title was not provided.
2652 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002653 else:
2654 default_title = RunGit(
2655 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002656 if options.force:
2657 title = default_title
2658 else:
2659 title = ask_for_data(
2660 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661 change_id = self._GetChangeDetail()['change_id']
2662 while True:
2663 footer_change_ids = git_footers.get_footer_change_id(message)
2664 if footer_change_ids == [change_id]:
2665 break
2666 if not footer_change_ids:
2667 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002668 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002669 continue
2670 # There is already a valid footer but with different or several ids.
2671 # Doing this automatically is non-trivial as we don't want to lose
2672 # existing other footers, yet we want to append just 1 desired
2673 # Change-Id. Thus, just create a new footer, but let user verify the
2674 # new description.
2675 message = '%s\n\nChange-Id: %s' % (message, change_id)
2676 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002677 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002679 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002680 'Please, check the proposed correction to the description, '
2681 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2682 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2683 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002684 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002685 if not options.force:
2686 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002687 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002688 message = change_desc.description
2689 if not message:
2690 DieWithError("Description is empty. Aborting...")
2691 # Continue the while loop.
2692 # Sanity check of this code - we should end up with proper message
2693 # footer.
2694 assert [change_id] == git_footers.get_footer_change_id(message)
2695 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002696 else: # if not self.GetIssue()
2697 if options.message:
2698 message = options.message
2699 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002700 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002701 if options.title:
2702 message = options.title + '\n\n' + message
2703 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002704
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002706 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002707 # On first upload, patchset title is always this string, while
2708 # --title flag gets converted to first line of message.
2709 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002710 if not change_desc.description:
2711 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002712 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002713 if len(change_ids) > 1:
2714 DieWithError('too many Change-Id footers, at most 1 allowed.')
2715 if not change_ids:
2716 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002717 change_desc.set_description(git_footers.add_footer_change_id(
2718 change_desc.description,
2719 GenerateGerritChangeId(change_desc.description)))
2720 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 assert len(change_ids) == 1
2722 change_id = change_ids[0]
2723
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002724 if options.reviewers or options.tbrs or options.add_owners_to:
2725 change_desc.update_reviewers(options.reviewers, options.tbrs,
2726 options.add_owners_to, change)
2727
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002729 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2730 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002732 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2733 desc_tempfile.write(change_desc.description)
2734 desc_tempfile.close()
2735 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2736 '-F', desc_tempfile.name]).strip()
2737 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738 else:
2739 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002740 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 if not change_desc.description:
2742 DieWithError("Description is empty. Aborting...")
2743
2744 if not git_footers.get_footer_change_id(change_desc.description):
2745 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002746 change_desc.set_description(
2747 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002748 if options.reviewers or options.tbrs or options.add_owners_to:
2749 change_desc.update_reviewers(options.reviewers, options.tbrs,
2750 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002752 # For no-squash mode, we assume the remote called "origin" is the one we
2753 # want. It is not worthwhile to support different workflows for
2754 # no-squash mode.
2755 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2757
2758 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002759 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002760 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2761 ref_to_push)]).splitlines()
2762 if len(commits) > 1:
2763 print('WARNING: This will upload %d commits. Run the following command '
2764 'to see which commits will be uploaded: ' % len(commits))
2765 print('git log %s..%s' % (parent, ref_to_push))
2766 print('You can also use `git squash-branch` to squash these into a '
2767 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002768 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002770 if options.reviewers or options.tbrs or options.add_owners_to:
2771 change_desc.update_reviewers(options.reviewers, options.tbrs,
2772 options.add_owners_to, change)
2773
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002774 reviewers = sorted(change_desc.get_reviewers())
2775 # Add cc's from the CC_LIST and --cc flag (if any).
2776 if not options.private and not options.no_autocc:
2777 cc = self.GetCCList().split(',')
2778 else:
2779 cc = []
2780 if options.cc:
2781 cc.extend(options.cc)
2782 cc = filter(None, [email.strip() for email in cc])
2783 if change_desc.get_cced():
2784 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002785 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2786 valid_accounts = set(reviewers + cc)
2787 # TODO(crbug/877717): relax this for all hosts.
2788 else:
2789 valid_accounts = gerrit_util.ValidAccounts(
2790 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002791 logging.info('accounts %s are recognized, %s invalid',
2792 sorted(valid_accounts),
2793 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002794
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002795 # Extra options that can be specified at push time. Doc:
2796 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002797 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002798
Aaron Gable844cf292017-06-28 11:32:59 -07002799 # By default, new changes are started in WIP mode, and subsequent patchsets
2800 # don't send email. At any time, passing --send-mail will mark the change
2801 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002802 if options.send_mail:
2803 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002804 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002805 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002806 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002807 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002808 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002809
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002810 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002811 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002812
Aaron Gable9b713dd2016-12-14 16:04:21 -08002813 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002814 # Punctuation and whitespace in |title| must be percent-encoded.
2815 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002816
agablec6787972016-09-09 16:13:34 -07002817 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002818 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002819
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002820 for r in sorted(reviewers):
2821 if r in valid_accounts:
2822 refspec_opts.append('r=%s' % r)
2823 reviewers.remove(r)
2824 else:
2825 # TODO(tandrii): this should probably be a hard failure.
2826 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2827 % r)
2828 for c in sorted(cc):
2829 # refspec option will be rejected if cc doesn't correspond to an
2830 # account, even though REST call to add such arbitrary cc may succeed.
2831 if c in valid_accounts:
2832 refspec_opts.append('cc=%s' % c)
2833 cc.remove(c)
2834
rmistry9eadede2016-09-19 11:22:43 -07002835 if options.topic:
2836 # Documentation on Gerrit topics is here:
2837 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002838 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002839
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002840 if not change_desc.get_reviewers(tbr_only=True):
2841 # Change is not TBR, so we can inline setting other labels, too.
2842 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2843 # max score for CR label somehow.
2844 if options.enable_auto_submit:
2845 refspec_opts.append('l=Auto-Submit+1')
2846 if options.use_commit_queue:
2847 refspec_opts.append('l=Commit-Queue+2')
2848 elif options.cq_dry_run:
2849 refspec_opts.append('l=Commit-Queue+1')
2850
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002851 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002852 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002853 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002854 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002855 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2856
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002857 refspec_suffix = ''
2858 if refspec_opts:
2859 refspec_suffix = '%' + ','.join(refspec_opts)
2860 assert ' ' not in refspec_suffix, (
2861 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2862 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2863
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002864 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002865 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002866 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002867 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002868 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002869 # Flush after every line: useful for seeing progress when running as
2870 # recipe.
2871 filter_fn=lambda _: sys.stdout.flush())
2872 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002873 except subprocess2.CalledProcessError as e:
2874 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002875 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002876 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002877 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002878 'credential problems:\n'
2879 ' git cl creds-check\n',
2880 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002881 finally:
2882 metrics.collector.add_repeated('sub_commands', {
2883 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002884 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002885 'exit_code': push_returncode,
2886 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2887 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002888
2889 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002890 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002891 change_numbers = [m.group(1)
2892 for m in map(regex.match, push_stdout.splitlines())
2893 if m]
2894 if len(change_numbers) != 1:
2895 DieWithError(
2896 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002897 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002898 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002899 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002900
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002901 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002902 # GetIssue() is not set in case of non-squash uploads according to tests.
2903 # TODO(agable): non-squash uploads in git cl should be removed.
2904 gerrit_util.AddReviewers(
2905 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002906 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002907 reviewers, cc,
2908 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002909
Aaron Gablefd238082017-06-07 13:42:34 -07002910 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002911 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
2912 score = 1
2913 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
2914 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
2915 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07002916 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002917 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002918 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09002919 msg='Self-approving for TBR',
2920 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002921 # Labels aren't set through refspec only if tbr is set (see check above).
2922 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
2923 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002924 return 0
2925
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002926 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2927 change_desc):
2928 """Computes parent of the generated commit to be uploaded to Gerrit.
2929
2930 Returns revision or a ref name.
2931 """
2932 if custom_cl_base:
2933 # Try to avoid creating additional unintended CLs when uploading, unless
2934 # user wants to take this risk.
2935 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2936 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2937 local_ref_of_target_remote])
2938 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002939 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002940 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2941 'If you proceed with upload, more than 1 CL may be created by '
2942 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2943 'If you are certain that specified base `%s` has already been '
2944 'uploaded to Gerrit as another CL, you may proceed.\n' %
2945 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2946 if not force:
2947 confirm_or_exit(
2948 'Do you take responsibility for cleaning up potential mess '
2949 'resulting from proceeding with upload?',
2950 action='upload')
2951 return custom_cl_base
2952
Aaron Gablef97e33d2017-03-30 15:44:27 -07002953 if remote != '.':
2954 return self.GetCommonAncestorWithUpstream()
2955
2956 # If our upstream branch is local, we base our squashed commit on its
2957 # squashed version.
2958 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2959
Aaron Gablef97e33d2017-03-30 15:44:27 -07002960 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002961 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002962
2963 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002964 # TODO(tandrii): consider checking parent change in Gerrit and using its
2965 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2966 # the tree hash of the parent branch. The upside is less likely bogus
2967 # requests to reupload parent change just because it's uploadhash is
2968 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002969 parent = RunGit(['config',
2970 'branch.%s.gerritsquashhash' % upstream_branch_name],
2971 error_ok=True).strip()
2972 # Verify that the upstream branch has been uploaded too, otherwise
2973 # Gerrit will create additional CLs when uploading.
2974 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2975 RunGitSilent(['rev-parse', parent + ':'])):
2976 DieWithError(
2977 '\nUpload upstream branch %s first.\n'
2978 'It is likely that this branch has been rebased since its last '
2979 'upload, so you just need to upload it again.\n'
2980 '(If you uploaded it with --no-squash, then branch dependencies '
2981 'are not supported, and you should reupload with --squash.)'
2982 % upstream_branch_name,
2983 change_desc)
2984 return parent
2985
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002986 def _AddChangeIdToCommitMessage(self, options, args):
2987 """Re-commits using the current message, assumes the commit hook is in
2988 place.
2989 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002990 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002991 git_command = ['commit', '--amend', '-m', log_desc]
2992 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002993 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002994 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002995 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002996 return new_log_desc
2997 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002998 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002999
Ravi Mistry31e7d562018-04-02 12:53:57 -04003000 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3001 """Sets labels on the change based on the provided flags."""
3002 labels = {}
3003 notify = None;
3004 if enable_auto_submit:
3005 labels['Auto-Submit'] = 1
3006 if use_commit_queue:
3007 labels['Commit-Queue'] = 2
3008 elif cq_dry_run:
3009 labels['Commit-Queue'] = 1
3010 notify = False
3011 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003012 gerrit_util.SetReview(
3013 self._GetGerritHost(),
3014 self._GerritChangeIdentifier(),
3015 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003016
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003017 def SetCQState(self, new_state):
3018 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003019 vote_map = {
3020 _CQState.NONE: 0,
3021 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003022 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003023 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003024 labels = {'Commit-Queue': vote_map[new_state]}
3025 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003026 gerrit_util.SetReview(
3027 self._GetGerritHost(), self._GerritChangeIdentifier(),
3028 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003029
tandriie113dfd2016-10-11 10:20:12 -07003030 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003031 try:
3032 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003033 except GerritChangeNotExists:
3034 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003035
3036 if data['status'] in ('ABANDONED', 'MERGED'):
3037 return 'CL %s is closed' % self.GetIssue()
3038
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003039 def GetTryJobProperties(self, patchset=None):
3040 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003041 data = self._GetChangeDetail(['ALL_REVISIONS'])
3042 patchset = int(patchset or self.GetPatchset())
3043 assert patchset
3044 revision_data = None # Pylint wants it to be defined.
3045 for revision_data in data['revisions'].itervalues():
3046 if int(revision_data['_number']) == patchset:
3047 break
3048 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003049 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003050 (patchset, self.GetIssue()))
3051 return {
3052 'patch_issue': self.GetIssue(),
3053 'patch_set': patchset or self.GetPatchset(),
3054 'patch_project': data['project'],
3055 'patch_storage': 'gerrit',
3056 'patch_ref': revision_data['fetch']['http']['ref'],
3057 'patch_repository_url': revision_data['fetch']['http']['url'],
3058 'patch_gerrit_url': self.GetCodereviewServer(),
3059 }
tandriie113dfd2016-10-11 10:20:12 -07003060
tandriide281ae2016-10-12 06:02:30 -07003061 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003062 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003063
Edward Lemur707d70b2018-02-07 00:50:14 +01003064 def GetReviewers(self):
3065 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003066 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003067
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003068
3069_CODEREVIEW_IMPLEMENTATIONS = {
3070 'rietveld': _RietveldChangelistImpl,
3071 'gerrit': _GerritChangelistImpl,
3072}
3073
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003074
iannuccie53c9352016-08-17 14:40:40 -07003075def _add_codereview_issue_select_options(parser, extra=""):
3076 _add_codereview_select_options(parser)
3077
3078 text = ('Operate on this issue number instead of the current branch\'s '
3079 'implicit issue.')
3080 if extra:
3081 text += ' '+extra
3082 parser.add_option('-i', '--issue', type=int, help=text)
3083
3084
3085def _process_codereview_issue_select_options(parser, options):
3086 _process_codereview_select_options(parser, options)
3087 if options.issue is not None and not options.forced_codereview:
3088 parser.error('--issue must be specified with either --rietveld or --gerrit')
3089
3090
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003091def _add_codereview_select_options(parser):
3092 """Appends --gerrit and --rietveld options to force specific codereview."""
3093 parser.codereview_group = optparse.OptionGroup(
3094 parser, 'EXPERIMENTAL! Codereview override options')
3095 parser.add_option_group(parser.codereview_group)
3096 parser.codereview_group.add_option(
3097 '--gerrit', action='store_true',
3098 help='Force the use of Gerrit for codereview')
3099 parser.codereview_group.add_option(
3100 '--rietveld', action='store_true',
3101 help='Force the use of Rietveld for codereview')
3102
3103
3104def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003105 if options.rietveld:
3106 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003107 options.forced_codereview = None
3108 if options.gerrit:
3109 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003110
3111
tandriif9aefb72016-07-01 09:06:51 -07003112def _get_bug_line_values(default_project, bugs):
3113 """Given default_project and comma separated list of bugs, yields bug line
3114 values.
3115
3116 Each bug can be either:
3117 * a number, which is combined with default_project
3118 * string, which is left as is.
3119
3120 This function may produce more than one line, because bugdroid expects one
3121 project per line.
3122
3123 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3124 ['v8:123', 'chromium:789']
3125 """
3126 default_bugs = []
3127 others = []
3128 for bug in bugs.split(','):
3129 bug = bug.strip()
3130 if bug:
3131 try:
3132 default_bugs.append(int(bug))
3133 except ValueError:
3134 others.append(bug)
3135
3136 if default_bugs:
3137 default_bugs = ','.join(map(str, default_bugs))
3138 if default_project:
3139 yield '%s:%s' % (default_project, default_bugs)
3140 else:
3141 yield default_bugs
3142 for other in sorted(others):
3143 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3144 yield other
3145
3146
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003147class ChangeDescription(object):
3148 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003149 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003150 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003151 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003152 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003153 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3154 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3155 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3156 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003157
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003158 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003159 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003160
agable@chromium.org42c20792013-09-12 17:34:49 +00003161 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003162 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003163 return '\n'.join(self._description_lines)
3164
3165 def set_description(self, desc):
3166 if isinstance(desc, basestring):
3167 lines = desc.splitlines()
3168 else:
3169 lines = [line.rstrip() for line in desc]
3170 while lines and not lines[0]:
3171 lines.pop(0)
3172 while lines and not lines[-1]:
3173 lines.pop(-1)
3174 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003175
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003176 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3177 """Rewrites the R=/TBR= line(s) as a single line each.
3178
3179 Args:
3180 reviewers (list(str)) - list of additional emails to use for reviewers.
3181 tbrs (list(str)) - list of additional emails to use for TBRs.
3182 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3183 the change that are missing OWNER coverage. If this is not None, you
3184 must also pass a value for `change`.
3185 change (Change) - The Change that should be used for OWNERS lookups.
3186 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003187 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003188 assert isinstance(tbrs, list), tbrs
3189
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003190 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003191 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003192
3193 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003194 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003195
3196 reviewers = set(reviewers)
3197 tbrs = set(tbrs)
3198 LOOKUP = {
3199 'TBR': tbrs,
3200 'R': reviewers,
3201 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003202
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003203 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003204 regexp = re.compile(self.R_LINE)
3205 matches = [regexp.match(line) for line in self._description_lines]
3206 new_desc = [l for i, l in enumerate(self._description_lines)
3207 if not matches[i]]
3208 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003209
agable@chromium.org42c20792013-09-12 17:34:49 +00003210 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003211
3212 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003213 for match in matches:
3214 if not match:
3215 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003216 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3217
3218 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003219 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003220 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003221 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003222 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003223 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003224 LOOKUP[add_owners_to].update(
3225 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003226
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003227 # If any folks ended up in both groups, remove them from tbrs.
3228 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003229
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003230 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3231 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003232
3233 # Put the new lines in the description where the old first R= line was.
3234 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3235 if 0 <= line_loc < len(self._description_lines):
3236 if new_tbr_line:
3237 self._description_lines.insert(line_loc, new_tbr_line)
3238 if new_r_line:
3239 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003240 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003241 if new_r_line:
3242 self.append_footer(new_r_line)
3243 if new_tbr_line:
3244 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003245
Aaron Gable3a16ed12017-03-23 10:51:55 -07003246 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003247 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003248 self.set_description([
3249 '# Enter a description of the change.',
3250 '# This will be displayed on the codereview site.',
3251 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003252 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003253 '--------------------',
3254 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003255
agable@chromium.org42c20792013-09-12 17:34:49 +00003256 regexp = re.compile(self.BUG_LINE)
3257 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003258 prefix = settings.GetBugPrefix()
3259 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003260 if git_footer:
3261 self.append_footer('Bug: %s' % ', '.join(values))
3262 else:
3263 for value in values:
3264 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003265
agable@chromium.org42c20792013-09-12 17:34:49 +00003266 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003267 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003268 if not content:
3269 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003270 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003271
Bruce Dawson2377b012018-01-11 16:46:49 -08003272 # Strip off comments and default inserted "Bug:" line.
3273 clean_lines = [line.rstrip() for line in lines if not
3274 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003275 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003276 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003277 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003278
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003279 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003280 """Adds a footer line to the description.
3281
3282 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3283 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3284 that Gerrit footers are always at the end.
3285 """
3286 parsed_footer_line = git_footers.parse_footer(line)
3287 if parsed_footer_line:
3288 # Line is a gerrit footer in the form: Footer-Key: any value.
3289 # Thus, must be appended observing Gerrit footer rules.
3290 self.set_description(
3291 git_footers.add_footer(self.description,
3292 key=parsed_footer_line[0],
3293 value=parsed_footer_line[1]))
3294 return
3295
3296 if not self._description_lines:
3297 self._description_lines.append(line)
3298 return
3299
3300 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3301 if gerrit_footers:
3302 # git_footers.split_footers ensures that there is an empty line before
3303 # actual (gerrit) footers, if any. We have to keep it that way.
3304 assert top_lines and top_lines[-1] == ''
3305 top_lines, separator = top_lines[:-1], top_lines[-1:]
3306 else:
3307 separator = [] # No need for separator if there are no gerrit_footers.
3308
3309 prev_line = top_lines[-1] if top_lines else ''
3310 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3311 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3312 top_lines.append('')
3313 top_lines.append(line)
3314 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003315
tandrii99a72f22016-08-17 14:33:24 -07003316 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003317 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003318 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003319 reviewers = [match.group(2).strip()
3320 for match in matches
3321 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003322 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003323
bradnelsond975b302016-10-23 12:20:23 -07003324 def get_cced(self):
3325 """Retrieves the list of reviewers."""
3326 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3327 cced = [match.group(2).strip() for match in matches if match]
3328 return cleanup_list(cced)
3329
Nodir Turakulov23b82142017-11-16 11:04:25 -08003330 def get_hash_tags(self):
3331 """Extracts and sanitizes a list of Gerrit hashtags."""
3332 subject = (self._description_lines or ('',))[0]
3333 subject = re.sub(
3334 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3335
3336 tags = []
3337 start = 0
3338 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3339 while True:
3340 m = bracket_exp.match(subject, start)
3341 if not m:
3342 break
3343 tags.append(self.sanitize_hash_tag(m.group(1)))
3344 start = m.end()
3345
3346 if not tags:
3347 # Try "Tag: " prefix.
3348 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3349 if m:
3350 tags.append(self.sanitize_hash_tag(m.group(1)))
3351 return tags
3352
3353 @classmethod
3354 def sanitize_hash_tag(cls, tag):
3355 """Returns a sanitized Gerrit hash tag.
3356
3357 A sanitized hashtag can be used as a git push refspec parameter value.
3358 """
3359 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3360
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003361 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3362 """Updates this commit description given the parent.
3363
3364 This is essentially what Gnumbd used to do.
3365 Consult https://goo.gl/WMmpDe for more details.
3366 """
3367 assert parent_msg # No, orphan branch creation isn't supported.
3368 assert parent_hash
3369 assert dest_ref
3370 parent_footer_map = git_footers.parse_footers(parent_msg)
3371 # This will also happily parse svn-position, which GnumbD is no longer
3372 # supporting. While we'd generate correct footers, the verifier plugin
3373 # installed in Gerrit will block such commit (ie git push below will fail).
3374 parent_position = git_footers.get_position(parent_footer_map)
3375
3376 # Cherry-picks may have last line obscuring their prior footers,
3377 # from git_footers perspective. This is also what Gnumbd did.
3378 cp_line = None
3379 if (self._description_lines and
3380 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3381 cp_line = self._description_lines.pop()
3382
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003383 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003384
3385 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3386 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003387 for i, line in enumerate(footer_lines):
3388 k, v = git_footers.parse_footer(line) or (None, None)
3389 if k and k.startswith('Cr-'):
3390 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003391
3392 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003393 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003394 if parent_position[0] == dest_ref:
3395 # Same branch as parent.
3396 number = int(parent_position[1]) + 1
3397 else:
3398 number = 1 # New branch, and extra lineage.
3399 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3400 int(parent_position[1])))
3401
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003402 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3403 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003404
3405 self._description_lines = top_lines
3406 if cp_line:
3407 self._description_lines.append(cp_line)
3408 if self._description_lines[-1] != '':
3409 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003410 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003411
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003412
Aaron Gablea1bab272017-04-11 16:38:18 -07003413def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003414 """Retrieves the reviewers that approved a CL from the issue properties with
3415 messages.
3416
3417 Note that the list may contain reviewers that are not committer, thus are not
3418 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003419
3420 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003421 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003422 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003423 return sorted(
3424 set(
3425 message['sender']
3426 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003427 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003428 )
3429 )
3430
3431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003432def FindCodereviewSettingsFile(filename='codereview.settings'):
3433 """Finds the given file starting in the cwd and going up.
3434
3435 Only looks up to the top of the repository unless an
3436 'inherit-review-settings-ok' file exists in the root of the repository.
3437 """
3438 inherit_ok_file = 'inherit-review-settings-ok'
3439 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003440 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003441 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3442 root = '/'
3443 while True:
3444 if filename in os.listdir(cwd):
3445 if os.path.isfile(os.path.join(cwd, filename)):
3446 return open(os.path.join(cwd, filename))
3447 if cwd == root:
3448 break
3449 cwd = os.path.dirname(cwd)
3450
3451
3452def LoadCodereviewSettingsFromFile(fileobj):
3453 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003454 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003455
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003456 def SetProperty(name, setting, unset_error_ok=False):
3457 fullname = 'rietveld.' + name
3458 if setting in keyvals:
3459 RunGit(['config', fullname, keyvals[setting]])
3460 else:
3461 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3462
tandrii48df5812016-10-17 03:55:37 -07003463 if not keyvals.get('GERRIT_HOST', False):
3464 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003465 # Only server setting is required. Other settings can be absent.
3466 # In that case, we ignore errors raised during option deletion attempt.
3467 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003468 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003469 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3470 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003471 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003472 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3473 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003474 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003475 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3476 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003477
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003478 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003479 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003480
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003481 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003482 RunGit(['config', 'gerrit.squash-uploads',
3483 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003484
tandrii@chromium.org28253532016-04-14 13:46:56 +00003485 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003486 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003487 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3488
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003490 # should be of the form
3491 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3492 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003493 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3494 keyvals['ORIGIN_URL_CONFIG']])
3495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003497def urlretrieve(source, destination):
3498 """urllib is broken for SSL connections via a proxy therefore we
3499 can't use urllib.urlretrieve()."""
3500 with open(destination, 'w') as f:
3501 f.write(urllib2.urlopen(source).read())
3502
3503
ukai@chromium.org712d6102013-11-27 00:52:58 +00003504def hasSheBang(fname):
3505 """Checks fname is a #! script."""
3506 with open(fname) as f:
3507 return f.read(2).startswith('#!')
3508
3509
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003510# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3511def DownloadHooks(*args, **kwargs):
3512 pass
3513
3514
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003515def DownloadGerritHook(force):
3516 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003517
3518 Args:
3519 force: True to update hooks. False to install hooks if not present.
3520 """
3521 if not settings.GetIsGerrit():
3522 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003523 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003524 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3525 if not os.access(dst, os.X_OK):
3526 if os.path.exists(dst):
3527 if not force:
3528 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003529 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003530 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003531 if not hasSheBang(dst):
3532 DieWithError('Not a script: %s\n'
3533 'You need to download from\n%s\n'
3534 'into .git/hooks/commit-msg and '
3535 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003536 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3537 except Exception:
3538 if os.path.exists(dst):
3539 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003540 DieWithError('\nFailed to download hooks.\n'
3541 'You need to download from\n%s\n'
3542 'into .git/hooks/commit-msg and '
3543 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003544
3545
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003546def GetRietveldCodereviewSettingsInteractively():
3547 """Prompt the user for settings."""
3548 server = settings.GetDefaultServerUrl(error_ok=True)
3549 prompt = 'Rietveld server (host[:port])'
3550 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3551 newserver = ask_for_data(prompt + ':')
3552 if not server and not newserver:
3553 newserver = DEFAULT_SERVER
3554 if newserver:
3555 newserver = gclient_utils.UpgradeToHttps(newserver)
3556 if newserver != server:
3557 RunGit(['config', 'rietveld.server', newserver])
3558
3559 def SetProperty(initial, caption, name, is_url):
3560 prompt = caption
3561 if initial:
3562 prompt += ' ("x" to clear) [%s]' % initial
3563 new_val = ask_for_data(prompt + ':')
3564 if new_val == 'x':
3565 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3566 elif new_val:
3567 if is_url:
3568 new_val = gclient_utils.UpgradeToHttps(new_val)
3569 if new_val != initial:
3570 RunGit(['config', 'rietveld.' + name, new_val])
3571
3572 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3573 SetProperty(settings.GetDefaultPrivateFlag(),
3574 'Private flag (rietveld only)', 'private', False)
3575 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3576 'tree-status-url', False)
3577 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3578 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3579 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3580 'run-post-upload-hook', False)
3581
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003582
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003583class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003584 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003585
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003586 _GOOGLESOURCE = 'googlesource.com'
3587
3588 def __init__(self):
3589 # Cached list of [host, identity, source], where source is either
3590 # .gitcookies or .netrc.
3591 self._all_hosts = None
3592
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003593 def ensure_configured_gitcookies(self):
3594 """Runs checks and suggests fixes to make git use .gitcookies from default
3595 path."""
3596 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3597 configured_path = RunGitSilent(
3598 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003599 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003600 if configured_path:
3601 self._ensure_default_gitcookies_path(configured_path, default)
3602 else:
3603 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003604
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003605 @staticmethod
3606 def _ensure_default_gitcookies_path(configured_path, default_path):
3607 assert configured_path
3608 if configured_path == default_path:
3609 print('git is already configured to use your .gitcookies from %s' %
3610 configured_path)
3611 return
3612
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003613 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003614 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3615 (configured_path, default_path))
3616
3617 if not os.path.exists(configured_path):
3618 print('However, your configured .gitcookies file is missing.')
3619 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3620 action='reconfigure')
3621 RunGit(['config', '--global', 'http.cookiefile', default_path])
3622 return
3623
3624 if os.path.exists(default_path):
3625 print('WARNING: default .gitcookies file already exists %s' %
3626 default_path)
3627 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3628 default_path)
3629
3630 confirm_or_exit('Move existing .gitcookies to default location?',
3631 action='move')
3632 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003633 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003634 print('Moved and reconfigured git to use .gitcookies from %s' %
3635 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003636
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003637 @staticmethod
3638 def _configure_gitcookies_path(default_path):
3639 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3640 if os.path.exists(netrc_path):
3641 print('You seem to be using outdated .netrc for git credentials: %s' %
3642 netrc_path)
3643 print('This tool will guide you through setting up recommended '
3644 '.gitcookies store for git credentials.\n'
3645 '\n'
3646 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3647 ' git config --global --unset http.cookiefile\n'
3648 ' mv %s %s.backup\n\n' % (default_path, default_path))
3649 confirm_or_exit(action='setup .gitcookies')
3650 RunGit(['config', '--global', 'http.cookiefile', default_path])
3651 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003652
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003653 def get_hosts_with_creds(self, include_netrc=False):
3654 if self._all_hosts is None:
3655 a = gerrit_util.CookiesAuthenticator()
3656 self._all_hosts = [
3657 (h, u, s)
3658 for h, u, s in itertools.chain(
3659 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3660 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3661 )
3662 if h.endswith(self._GOOGLESOURCE)
3663 ]
3664
3665 if include_netrc:
3666 return self._all_hosts
3667 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3668
3669 def print_current_creds(self, include_netrc=False):
3670 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3671 if not hosts:
3672 print('No Git/Gerrit credentials found')
3673 return
3674 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3675 header = [('Host', 'User', 'Which file'),
3676 ['=' * l for l in lengths]]
3677 for row in (header + hosts):
3678 print('\t'.join((('%%+%ds' % l) % s)
3679 for l, s in zip(lengths, row)))
3680
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003681 @staticmethod
3682 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003683 """Parses identity "git-<username>.domain" into <username> and domain."""
3684 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003685 # distinguishable from sub-domains. But we do know typical domains:
3686 if identity.endswith('.chromium.org'):
3687 domain = 'chromium.org'
3688 username = identity[:-len('.chromium.org')]
3689 else:
3690 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003691 if username.startswith('git-'):
3692 username = username[len('git-'):]
3693 return username, domain
3694
3695 def _get_usernames_of_domain(self, domain):
3696 """Returns list of usernames referenced by .gitcookies in a given domain."""
3697 identities_by_domain = {}
3698 for _, identity, _ in self.get_hosts_with_creds():
3699 username, domain = self._parse_identity(identity)
3700 identities_by_domain.setdefault(domain, []).append(username)
3701 return identities_by_domain.get(domain)
3702
3703 def _canonical_git_googlesource_host(self, host):
3704 """Normalizes Gerrit hosts (with '-review') to Git host."""
3705 assert host.endswith(self._GOOGLESOURCE)
3706 # Prefix doesn't include '.' at the end.
3707 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3708 if prefix.endswith('-review'):
3709 prefix = prefix[:-len('-review')]
3710 return prefix + '.' + self._GOOGLESOURCE
3711
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003712 def _canonical_gerrit_googlesource_host(self, host):
3713 git_host = self._canonical_git_googlesource_host(host)
3714 prefix = git_host.split('.', 1)[0]
3715 return prefix + '-review.' + self._GOOGLESOURCE
3716
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003717 def _get_counterpart_host(self, host):
3718 assert host.endswith(self._GOOGLESOURCE)
3719 git = self._canonical_git_googlesource_host(host)
3720 gerrit = self._canonical_gerrit_googlesource_host(git)
3721 return git if gerrit == host else gerrit
3722
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003723 def has_generic_host(self):
3724 """Returns whether generic .googlesource.com has been configured.
3725
3726 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3727 """
3728 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3729 if host == '.' + self._GOOGLESOURCE:
3730 return True
3731 return False
3732
3733 def _get_git_gerrit_identity_pairs(self):
3734 """Returns map from canonic host to pair of identities (Git, Gerrit).
3735
3736 One of identities might be None, meaning not configured.
3737 """
3738 host_to_identity_pairs = {}
3739 for host, identity, _ in self.get_hosts_with_creds():
3740 canonical = self._canonical_git_googlesource_host(host)
3741 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3742 idx = 0 if canonical == host else 1
3743 pair[idx] = identity
3744 return host_to_identity_pairs
3745
3746 def get_partially_configured_hosts(self):
3747 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003748 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3749 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3750 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003751
3752 def get_conflicting_hosts(self):
3753 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003754 host
3755 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003756 if None not in (i1, i2) and i1 != i2)
3757
3758 def get_duplicated_hosts(self):
3759 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3760 return set(host for host, count in counters.iteritems() if count > 1)
3761
3762 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3763 'chromium.googlesource.com': 'chromium.org',
3764 'chrome-internal.googlesource.com': 'google.com',
3765 }
3766
3767 def get_hosts_with_wrong_identities(self):
3768 """Finds hosts which **likely** reference wrong identities.
3769
3770 Note: skips hosts which have conflicting identities for Git and Gerrit.
3771 """
3772 hosts = set()
3773 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3774 pair = self._get_git_gerrit_identity_pairs().get(host)
3775 if pair and pair[0] == pair[1]:
3776 _, domain = self._parse_identity(pair[0])
3777 if domain != expected:
3778 hosts.add(host)
3779 return hosts
3780
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003781 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003782 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003783 hosts = sorted(hosts)
3784 assert hosts
3785 if extra_column_func is None:
3786 extras = [''] * len(hosts)
3787 else:
3788 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003789 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3790 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003791 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003792 lines.append(tmpl % he)
3793 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003794
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003795 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003796 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003797 yield ('.googlesource.com wildcard record detected',
3798 ['Chrome Infrastructure team recommends to list full host names '
3799 'explicitly.'],
3800 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003801
3802 dups = self.get_duplicated_hosts()
3803 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003804 yield ('The following hosts were defined twice',
3805 self._format_hosts(dups),
3806 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003807
3808 partial = self.get_partially_configured_hosts()
3809 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003810 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3811 'These hosts are missing',
3812 self._format_hosts(partial, lambda host: 'but %s defined' %
3813 self._get_counterpart_host(host)),
3814 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003815
3816 conflicting = self.get_conflicting_hosts()
3817 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003818 yield ('The following Git hosts have differing credentials from their '
3819 'Gerrit counterparts',
3820 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3821 tuple(self._get_git_gerrit_identity_pairs()[host])),
3822 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003823
3824 wrong = self.get_hosts_with_wrong_identities()
3825 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003826 yield ('These hosts likely use wrong identity',
3827 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3828 (self._get_git_gerrit_identity_pairs()[host][0],
3829 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3830 wrong)
3831
3832 def find_and_report_problems(self):
3833 """Returns True if there was at least one problem, else False."""
3834 found = False
3835 bad_hosts = set()
3836 for title, sublines, hosts in self._find_problems():
3837 if not found:
3838 found = True
3839 print('\n\n.gitcookies problem report:\n')
3840 bad_hosts.update(hosts or [])
3841 print(' %s%s' % (title , (':' if sublines else '')))
3842 if sublines:
3843 print()
3844 print(' %s' % '\n '.join(sublines))
3845 print()
3846
3847 if bad_hosts:
3848 assert found
3849 print(' You can manually remove corresponding lines in your %s file and '
3850 'visit the following URLs with correct account to generate '
3851 'correct credential lines:\n' %
3852 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3853 print(' %s' % '\n '.join(sorted(set(
3854 gerrit_util.CookiesAuthenticator().get_new_password_url(
3855 self._canonical_git_googlesource_host(host))
3856 for host in bad_hosts
3857 ))))
3858 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003859
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003860
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003861@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003862def CMDcreds_check(parser, args):
3863 """Checks credentials and suggests changes."""
3864 _, _ = parser.parse_args(args)
3865
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003866 # Code below checks .gitcookies. Abort if using something else.
3867 authn = gerrit_util.Authenticator.get()
3868 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3869 if isinstance(authn, gerrit_util.GceAuthenticator):
3870 DieWithError(
3871 'This command is not designed for GCE, are you on a bot?\n'
3872 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3873 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003874 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003875 'This command is not designed for bot environment. It checks '
3876 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003877
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003878 checker = _GitCookiesChecker()
3879 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003880
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003881 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003882 checker.print_current_creds(include_netrc=True)
3883
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003884 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003885 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003886 return 0
3887 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003888
3889
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003890@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003891@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003893 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003895 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07003896 # TODO(tandrii): remove this once we switch to Gerrit.
3897 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003898 parser.add_option('--activate-update', action='store_true',
3899 help='activate auto-updating [rietveld] section in '
3900 '.git/config')
3901 parser.add_option('--deactivate-update', action='store_true',
3902 help='deactivate auto-updating [rietveld] section in '
3903 '.git/config')
3904 options, args = parser.parse_args(args)
3905
3906 if options.deactivate_update:
3907 RunGit(['config', 'rietveld.autoupdate', 'false'])
3908 return
3909
3910 if options.activate_update:
3911 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3912 return
3913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003914 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003915 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916 return 0
3917
3918 url = args[0]
3919 if not url.endswith('codereview.settings'):
3920 url = os.path.join(url, 'codereview.settings')
3921
3922 # Load code review settings and download hooks (if available).
3923 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3924 return 0
3925
3926
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003927@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003928def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003929 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003930 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3931 branch = ShortBranchName(branchref)
3932 _, args = parser.parse_args(args)
3933 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003934 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003935 return RunGit(['config', 'branch.%s.base-url' % branch],
3936 error_ok=False).strip()
3937 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003938 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003939 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3940 error_ok=False).strip()
3941
3942
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003943def color_for_status(status):
3944 """Maps a Changelist status to color, for CMDstatus and other tools."""
3945 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003946 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003947 'waiting': Fore.BLUE,
3948 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003949 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003950 'lgtm': Fore.GREEN,
3951 'commit': Fore.MAGENTA,
3952 'closed': Fore.CYAN,
3953 'error': Fore.WHITE,
3954 }.get(status, Fore.WHITE)
3955
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003956
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003957def get_cl_statuses(changes, fine_grained, max_processes=None):
3958 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003959
3960 If fine_grained is true, this will fetch CL statuses from the server.
3961 Otherwise, simply indicate if there's a matching url for the given branches.
3962
3963 If max_processes is specified, it is used as the maximum number of processes
3964 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3965 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003966
3967 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003968 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003969 if not changes:
3970 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003971
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003972 if not fine_grained:
3973 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003974 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003975 for cl in changes:
3976 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003977 return
3978
3979 # First, sort out authentication issues.
3980 logging.debug('ensuring credentials exist')
3981 for cl in changes:
3982 cl.EnsureAuthenticated(force=False, refresh=True)
3983
3984 def fetch(cl):
3985 try:
3986 return (cl, cl.GetStatus())
3987 except:
3988 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003989 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003990 raise
3991
3992 threads_count = len(changes)
3993 if max_processes:
3994 threads_count = max(1, min(threads_count, max_processes))
3995 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3996
3997 pool = ThreadPool(threads_count)
3998 fetched_cls = set()
3999 try:
4000 it = pool.imap_unordered(fetch, changes).__iter__()
4001 while True:
4002 try:
4003 cl, status = it.next(timeout=5)
4004 except multiprocessing.TimeoutError:
4005 break
4006 fetched_cls.add(cl)
4007 yield cl, status
4008 finally:
4009 pool.close()
4010
4011 # Add any branches that failed to fetch.
4012 for cl in set(changes) - fetched_cls:
4013 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004014
rmistry@google.com2dd99862015-06-22 12:22:18 +00004015
4016def upload_branch_deps(cl, args):
4017 """Uploads CLs of local branches that are dependents of the current branch.
4018
4019 If the local branch dependency tree looks like:
4020 test1 -> test2.1 -> test3.1
4021 -> test3.2
4022 -> test2.2 -> test3.3
4023
4024 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4025 run on the dependent branches in this order:
4026 test2.1, test3.1, test3.2, test2.2, test3.3
4027
4028 Note: This function does not rebase your local dependent branches. Use it when
4029 you make a change to the parent branch that will not conflict with its
4030 dependent branches, and you would like their dependencies updated in
4031 Rietveld.
4032 """
4033 if git_common.is_dirty_git_tree('upload-branch-deps'):
4034 return 1
4035
4036 root_branch = cl.GetBranch()
4037 if root_branch is None:
4038 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4039 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004040 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004041 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4042 'patchset dependencies without an uploaded CL.')
4043
4044 branches = RunGit(['for-each-ref',
4045 '--format=%(refname:short) %(upstream:short)',
4046 'refs/heads'])
4047 if not branches:
4048 print('No local branches found.')
4049 return 0
4050
4051 # Create a dictionary of all local branches to the branches that are dependent
4052 # on it.
4053 tracked_to_dependents = collections.defaultdict(list)
4054 for b in branches.splitlines():
4055 tokens = b.split()
4056 if len(tokens) == 2:
4057 branch_name, tracked = tokens
4058 tracked_to_dependents[tracked].append(branch_name)
4059
vapiera7fbd5a2016-06-16 09:17:49 -07004060 print()
4061 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004062 dependents = []
4063 def traverse_dependents_preorder(branch, padding=''):
4064 dependents_to_process = tracked_to_dependents.get(branch, [])
4065 padding += ' '
4066 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004067 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004068 dependents.append(dependent)
4069 traverse_dependents_preorder(dependent, padding)
4070 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004071 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004072
4073 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004074 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004075 return 0
4076
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004077 confirm_or_exit('This command will checkout all dependent branches and run '
4078 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004079
rmistry@google.com2dd99862015-06-22 12:22:18 +00004080 # Record all dependents that failed to upload.
4081 failures = {}
4082 # Go through all dependents, checkout the branch and upload.
4083 try:
4084 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004085 print()
4086 print('--------------------------------------')
4087 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004088 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004090 try:
4091 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004092 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004093 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004094 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004095 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004096 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004097 finally:
4098 # Swap back to the original root branch.
4099 RunGit(['checkout', '-q', root_branch])
4100
vapiera7fbd5a2016-06-16 09:17:49 -07004101 print()
4102 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004103 for dependent_branch in dependents:
4104 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004105 print(' %s : %s' % (dependent_branch, upload_status))
4106 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004107
4108 return 0
4109
4110
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004111@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004112def CMDarchive(parser, args):
4113 """Archives and deletes branches associated with closed changelists."""
4114 parser.add_option(
4115 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004116 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004117 parser.add_option(
4118 '-f', '--force', action='store_true',
4119 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004120 parser.add_option(
4121 '-d', '--dry-run', action='store_true',
4122 help='Skip the branch tagging and removal steps.')
4123 parser.add_option(
4124 '-t', '--notags', action='store_true',
4125 help='Do not tag archived branches. '
4126 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004127
4128 auth.add_auth_options(parser)
4129 options, args = parser.parse_args(args)
4130 if args:
4131 parser.error('Unsupported args: %s' % ' '.join(args))
4132 auth_config = auth.extract_auth_config_from_options(options)
4133
4134 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4135 if not branches:
4136 return 0
4137
vapiera7fbd5a2016-06-16 09:17:49 -07004138 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004139 changes = [Changelist(branchref=b, auth_config=auth_config)
4140 for b in branches.splitlines()]
4141 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4142 statuses = get_cl_statuses(changes,
4143 fine_grained=True,
4144 max_processes=options.maxjobs)
4145 proposal = [(cl.GetBranch(),
4146 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4147 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004148 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004149 proposal.sort()
4150
4151 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004152 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004153 return 0
4154
4155 current_branch = GetCurrentBranch()
4156
vapiera7fbd5a2016-06-16 09:17:49 -07004157 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004158 if options.notags:
4159 for next_item in proposal:
4160 print(' ' + next_item[0])
4161 else:
4162 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4163 for next_item in proposal:
4164 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004165
kmarshall9249e012016-08-23 12:02:16 -07004166 # Quit now on precondition failure or if instructed by the user, either
4167 # via an interactive prompt or by command line flags.
4168 if options.dry_run:
4169 print('\nNo changes were made (dry run).\n')
4170 return 0
4171 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004172 print('You are currently on a branch \'%s\' which is associated with a '
4173 'closed codereview issue, so archive cannot proceed. Please '
4174 'checkout another branch and run this command again.' %
4175 current_branch)
4176 return 1
kmarshall9249e012016-08-23 12:02:16 -07004177 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004178 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4179 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004181 return 1
4182
4183 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004184 if not options.notags:
4185 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004186 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004187
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004189
4190 return 0
4191
4192
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004193@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004195 """Show status of changelists.
4196
4197 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004198 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004199 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004200 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004201 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004202 - Magenta in the commit queue
4203 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004204 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004205
4206 Also see 'git cl comments'.
4207 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004209 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004210 parser.add_option('-f', '--fast', action='store_true',
4211 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004212 parser.add_option(
4213 '-j', '--maxjobs', action='store', type=int,
4214 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004215
4216 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004217 _add_codereview_issue_select_options(
4218 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004219 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004220 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004221 if args:
4222 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004223 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224
iannuccie53c9352016-08-17 14:40:40 -07004225 if options.issue is not None and not options.field:
4226 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004229 cl = Changelist(auth_config=auth_config, issue=options.issue,
4230 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004231 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004232 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233 elif options.field == 'id':
4234 issueid = cl.GetIssue()
4235 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004236 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004238 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004239 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004240 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004241 elif options.field == 'status':
4242 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243 elif options.field == 'url':
4244 url = cl.GetIssueURL()
4245 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004246 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004247 return 0
4248
4249 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4250 if not branches:
4251 print('No local branch found.')
4252 return 0
4253
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004254 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004255 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004256 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004257 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004258 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004259 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004260 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004261
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004262 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004263 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4264 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4265 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004266 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004267 c, status = output.next()
4268 branch_statuses[c.GetBranch()] = status
4269 status = branch_statuses.pop(branch)
4270 url = cl.GetIssueURL()
4271 if url and (not status or status == 'error'):
4272 # The issue probably doesn't exist anymore.
4273 url += ' (broken)'
4274
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004275 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004276 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004277 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004278 color = ''
4279 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004280 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004282 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004283 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004284
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004285
4286 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004287 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004288 print('Current branch: %s' % branch)
4289 for cl in changes:
4290 if cl.GetBranch() == branch:
4291 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004292 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004293 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004294 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004295 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004296 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print('Issue description:')
4298 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 return 0
4300
4301
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004302def colorize_CMDstatus_doc():
4303 """To be called once in main() to add colors to git cl status help."""
4304 colors = [i for i in dir(Fore) if i[0].isupper()]
4305
4306 def colorize_line(line):
4307 for color in colors:
4308 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004309 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004310 indent = len(line) - len(line.lstrip(' ')) + 1
4311 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4312 return line
4313
4314 lines = CMDstatus.__doc__.splitlines()
4315 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4316
4317
phajdan.jre328cf92016-08-22 04:12:17 -07004318def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004319 if path == '-':
4320 json.dump(contents, sys.stdout)
4321 else:
4322 with open(path, 'w') as f:
4323 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004324
4325
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004326@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004327@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004328def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004329 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330
4331 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004332 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004333 parser.add_option('-r', '--reverse', action='store_true',
4334 help='Lookup the branch(es) for the specified issues. If '
4335 'no issues are specified, all branches with mapped '
4336 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004337 parser.add_option('--json',
4338 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004339 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004340 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004341 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004342
dnj@chromium.org406c4402015-03-03 17:22:28 +00004343 if options.reverse:
4344 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004345 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004346 # Reverse issue lookup.
4347 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004348
4349 git_config = {}
4350 for config in RunGit(['config', '--get-regexp',
4351 r'branch\..*issue']).splitlines():
4352 name, _space, val = config.partition(' ')
4353 git_config[name] = val
4354
dnj@chromium.org406c4402015-03-03 17:22:28 +00004355 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004356 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4357 config_key = _git_branch_config_key(ShortBranchName(branch),
4358 cls.IssueConfigKey())
4359 issue = git_config.get(config_key)
4360 if issue:
4361 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004362 if not args:
4363 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004364 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004365 for issue in args:
4366 if not issue:
4367 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004368 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004369 print('Branch for issue number %s: %s' % (
4370 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004371 if options.json:
4372 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004373 return 0
4374
4375 if len(args) > 0:
4376 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4377 if not issue.valid:
4378 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4379 'or no argument to list it.\n'
4380 'Maybe you want to run git cl status?')
4381 cl = Changelist(codereview=issue.codereview)
4382 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004383 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004384 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004385 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4386 if options.json:
4387 write_json(options.json, {
4388 'issue': cl.GetIssue(),
4389 'issue_url': cl.GetIssueURL(),
4390 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 return 0
4392
4393
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004394@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004395def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004396 """Shows or posts review comments for any changelist."""
4397 parser.add_option('-a', '--add-comment', dest='comment',
4398 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004399 parser.add_option('-p', '--publish', action='store_true',
4400 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004401 parser.add_option('-i', '--issue', dest='issue',
4402 help='review issue id (defaults to current issue). '
4403 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004404 parser.add_option('-m', '--machine-readable', dest='readable',
4405 action='store_false', default=True,
4406 help='output comments in a format compatible with '
4407 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004408 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004409 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004410 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004411 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004412 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004413 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004414 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004415
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004416 issue = None
4417 if options.issue:
4418 try:
4419 issue = int(options.issue)
4420 except ValueError:
4421 DieWithError('A review issue id is expected to be a number')
4422
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004423 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4424
4425 if not cl.IsGerrit():
4426 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004427
4428 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004429 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004430 return 0
4431
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004432 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4433 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004434 for comment in summary:
4435 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004436 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004437 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004438 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004439 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004440 color = Fore.MAGENTA
4441 else:
4442 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004443 print('\n%s%s %s%s\n%s' % (
4444 color,
4445 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4446 comment.sender,
4447 Fore.RESET,
4448 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4449
smut@google.comc85ac942015-09-15 16:34:43 +00004450 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004451 def pre_serialize(c):
4452 dct = c.__dict__.copy()
4453 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4454 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004455 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004456 return 0
4457
4458
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004459@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004460@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004461def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004462 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004463 parser.add_option('-d', '--display', action='store_true',
4464 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004465 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004466 help='New description to set for this issue (- for stdin, '
4467 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004468 parser.add_option('-f', '--force', action='store_true',
4469 help='Delete any unpublished Gerrit edits for this issue '
4470 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004471
4472 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004473 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004474 options, args = parser.parse_args(args)
4475 _process_codereview_select_options(parser, options)
4476
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004477 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004478 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004479 target_issue_arg = ParseIssueNumberArgument(args[0],
4480 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004481 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004482 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004483
martiniss6eda05f2016-06-30 10:18:35 -07004484 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004485 'auth_config': auth.extract_auth_config_from_options(options),
4486 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004487 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004488 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004489 if target_issue_arg:
4490 kwargs['issue'] = target_issue_arg.issue
4491 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004492 if target_issue_arg.codereview and not options.forced_codereview:
4493 detected_codereview_from_url = True
4494 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004495
4496 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004497 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004498 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004499 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004500
4501 if detected_codereview_from_url:
4502 logging.info('canonical issue/change URL: %s (type: %s)\n',
4503 cl.GetIssueURL(), target_issue_arg.codereview)
4504
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004505 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004506
smut@google.com34fb6b12015-07-13 20:03:26 +00004507 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004508 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004509 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004510
4511 if options.new_description:
4512 text = options.new_description
4513 if text == '-':
4514 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004515 elif text == '+':
4516 base_branch = cl.GetCommonAncestorWithUpstream()
4517 change = cl.GetChange(base_branch, None, local_description=True)
4518 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004519
4520 description.set_description(text)
4521 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004522 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004523
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004524 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004525 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004526 return 0
4527
4528
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004529@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004530def CMDlint(parser, args):
4531 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004532 parser.add_option('--filter', action='append', metavar='-x,+y',
4533 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004534 auth.add_auth_options(parser)
4535 options, args = parser.parse_args(args)
4536 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004537
4538 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004539 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004540 try:
4541 import cpplint
4542 import cpplint_chromium
4543 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004544 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004545 return 1
4546
4547 # Change the current working directory before calling lint so that it
4548 # shows the correct base.
4549 previous_cwd = os.getcwd()
4550 os.chdir(settings.GetRoot())
4551 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004552 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004553 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4554 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004555 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004556 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004557 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004558
4559 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004560 command = args + files
4561 if options.filter:
4562 command = ['--filter=' + ','.join(options.filter)] + command
4563 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004564
4565 white_regex = re.compile(settings.GetLintRegex())
4566 black_regex = re.compile(settings.GetLintIgnoreRegex())
4567 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4568 for filename in filenames:
4569 if white_regex.match(filename):
4570 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004571 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004572 else:
4573 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4574 extra_check_functions)
4575 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004576 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004577 finally:
4578 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004579 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004580 if cpplint._cpplint_state.error_count != 0:
4581 return 1
4582 return 0
4583
4584
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004585@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004586def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004587 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004588 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004589 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004590 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004591 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004592 parser.add_option('--all', action='store_true',
4593 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004594 parser.add_option('--parallel', action='store_true',
4595 help='Run all tests specified by input_api.RunTests in all '
4596 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004597 auth.add_auth_options(parser)
4598 options, args = parser.parse_args(args)
4599 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004600
sbc@chromium.org71437c02015-04-09 19:29:40 +00004601 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004602 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004603 return 1
4604
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004605 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 if args:
4607 base_branch = args[0]
4608 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004609 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004610 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611
Aaron Gable8076c282017-11-29 14:39:41 -08004612 if options.all:
4613 base_change = cl.GetChange(base_branch, None)
4614 files = [('M', f) for f in base_change.AllFiles()]
4615 change = presubmit_support.GitChange(
4616 base_change.Name(),
4617 base_change.FullDescriptionText(),
4618 base_change.RepositoryRoot(),
4619 files,
4620 base_change.issue,
4621 base_change.patchset,
4622 base_change.author_email,
4623 base_change._upstream)
4624 else:
4625 change = cl.GetChange(base_branch, None)
4626
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004627 cl.RunHook(
4628 committing=not options.upload,
4629 may_prompt=False,
4630 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004631 change=change,
4632 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004633 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004634
4635
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004636def GenerateGerritChangeId(message):
4637 """Returns Ixxxxxx...xxx change id.
4638
4639 Works the same way as
4640 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4641 but can be called on demand on all platforms.
4642
4643 The basic idea is to generate git hash of a state of the tree, original commit
4644 message, author/committer info and timestamps.
4645 """
4646 lines = []
4647 tree_hash = RunGitSilent(['write-tree'])
4648 lines.append('tree %s' % tree_hash.strip())
4649 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4650 if code == 0:
4651 lines.append('parent %s' % parent.strip())
4652 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4653 lines.append('author %s' % author.strip())
4654 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4655 lines.append('committer %s' % committer.strip())
4656 lines.append('')
4657 # Note: Gerrit's commit-hook actually cleans message of some lines and
4658 # whitespace. This code is not doing this, but it clearly won't decrease
4659 # entropy.
4660 lines.append(message)
4661 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4662 stdin='\n'.join(lines))
4663 return 'I%s' % change_hash.strip()
4664
4665
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004666def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004667 """Computes the remote branch ref to use for the CL.
4668
4669 Args:
4670 remote (str): The git remote for the CL.
4671 remote_branch (str): The git remote branch for the CL.
4672 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004673 """
4674 if not (remote and remote_branch):
4675 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004676
wittman@chromium.org455dc922015-01-26 20:15:50 +00004677 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004678 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004679 # refs, which are then translated into the remote full symbolic refs
4680 # below.
4681 if '/' not in target_branch:
4682 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4683 else:
4684 prefix_replacements = (
4685 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4686 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4687 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4688 )
4689 match = None
4690 for regex, replacement in prefix_replacements:
4691 match = re.search(regex, target_branch)
4692 if match:
4693 remote_branch = target_branch.replace(match.group(0), replacement)
4694 break
4695 if not match:
4696 # This is a branch path but not one we recognize; use as-is.
4697 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004698 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4699 # Handle the refs that need to land in different refs.
4700 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004701
wittman@chromium.org455dc922015-01-26 20:15:50 +00004702 # Create the true path to the remote branch.
4703 # Does the following translation:
4704 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4705 # * refs/remotes/origin/master -> refs/heads/master
4706 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4707 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4708 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4709 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4710 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4711 'refs/heads/')
4712 elif remote_branch.startswith('refs/remotes/branch-heads'):
4713 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004714
wittman@chromium.org455dc922015-01-26 20:15:50 +00004715 return remote_branch
4716
4717
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004718def cleanup_list(l):
4719 """Fixes a list so that comma separated items are put as individual items.
4720
4721 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4722 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4723 """
4724 items = sum((i.split(',') for i in l), [])
4725 stripped_items = (i.strip() for i in items)
4726 return sorted(filter(None, stripped_items))
4727
4728
Aaron Gable4db38df2017-11-03 14:59:07 -07004729@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004730@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004731def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004732 """Uploads the current changelist to codereview.
4733
4734 Can skip dependency patchset uploads for a branch by running:
4735 git config branch.branch_name.skip-deps-uploads True
4736 To unset run:
4737 git config --unset branch.branch_name.skip-deps-uploads
4738 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004739
4740 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4741 a bug number, this bug number is automatically populated in the CL
4742 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004743
4744 If subject contains text in square brackets or has "<text>: " prefix, such
4745 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4746 [git-cl] add support for hashtags
4747 Foo bar: implement foo
4748 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004749 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004750 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4751 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004752 parser.add_option('--bypass-watchlists', action='store_true',
4753 dest='bypass_watchlists',
4754 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004755 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004756 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004757 parser.add_option('--message', '-m', dest='message',
4758 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004759 parser.add_option('-b', '--bug',
4760 help='pre-populate the bug number(s) for this issue. '
4761 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004762 parser.add_option('--message-file', dest='message_file',
4763 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004764 parser.add_option('--title', '-t', dest='title',
4765 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004766 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004767 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004768 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004769 parser.add_option('--tbrs',
4770 action='append', default=[],
4771 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004772 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004773 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004774 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004775 parser.add_option('--hashtag', dest='hashtags',
4776 action='append', default=[],
4777 help=('Gerrit hashtag for new CL; '
4778 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004779 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004780 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004781 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004782 help='tell the commit queue to commit this patchset; '
4783 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004784 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004785 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004786 metavar='TARGET',
4787 help='Apply CL to remote ref TARGET. ' +
4788 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004789 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004790 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004791 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004792 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004793 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004794 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004795 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4796 const='TBR', help='add a set of OWNERS to TBR')
4797 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4798 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004799 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4800 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004801 help='Send the patchset to do a CQ dry run right after '
4802 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004803 parser.add_option('--dependencies', action='store_true',
4804 help='Uploads CLs of all the local branches that depend on '
4805 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004806 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4807 help='Sends your change to the CQ after an approval. Only '
4808 'works on repos that have the Auto-Submit label '
4809 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004810 parser.add_option('--parallel', action='store_true',
4811 help='Run all tests specified by input_api.RunTests in all '
4812 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004813
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004814 parser.add_option('--no-autocc', action='store_true',
4815 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004816 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004817 help='Set the review private. This implies --no-autocc.')
4818
rmistry@google.com2dd99862015-06-22 12:22:18 +00004819 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004820 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004821 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004822 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004823 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004824 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004825
sbc@chromium.org71437c02015-04-09 19:29:40 +00004826 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004827 return 1
4828
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004829 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004830 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004831 options.cc = cleanup_list(options.cc)
4832
tandriib80458a2016-06-23 12:20:07 -07004833 if options.message_file:
4834 if options.message:
4835 parser.error('only one of --message and --message-file allowed.')
4836 options.message = gclient_utils.FileRead(options.message_file)
4837 options.message_file = None
4838
tandrii4d0545a2016-07-06 03:56:49 -07004839 if options.cq_dry_run and options.use_commit_queue:
4840 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4841
Aaron Gableedbc4132017-09-11 13:22:28 -07004842 if options.use_commit_queue:
4843 options.send_mail = True
4844
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004845 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4846 settings.GetIsGerrit()
4847
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004848 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004849 if not cl.IsGerrit():
4850 # Error out with instructions for repos not yet configured for Gerrit.
4851 print('=====================================')
4852 print('NOTICE: Rietveld is no longer supported. '
4853 'You can upload changes to Gerrit with')
4854 print(' git cl upload --gerrit')
4855 print('or set Gerrit to be your default code review tool with')
4856 print(' git config gerrit.host true')
4857 print('=====================================')
4858 return 1
4859
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004860 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004861
4862
Francois Dorayd42c6812017-05-30 15:10:20 -04004863@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004864@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004865def CMDsplit(parser, args):
4866 """Splits a branch into smaller branches and uploads CLs.
4867
4868 Creates a branch and uploads a CL for each group of files modified in the
4869 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004870 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004871 the shared OWNERS file.
4872 """
4873 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004874 help="A text file containing a CL description in which "
4875 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004876 parser.add_option("-c", "--comment", dest="comment_file",
4877 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004878 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4879 default=False,
4880 help="List the files and reviewers for each CL that would "
4881 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004882 parser.add_option("--cq-dry-run", action='store_true',
4883 help="If set, will do a cq dry run for each uploaded CL. "
4884 "Please be careful when doing this; more than ~10 CLs "
4885 "has the potential to overload our build "
4886 "infrastructure. Try to upload these not during high "
4887 "load times (usually 11-3 Mountain View time). Email "
4888 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004889 options, _ = parser.parse_args(args)
4890
4891 if not options.description_file:
4892 parser.error('No --description flag specified.')
4893
4894 def WrappedCMDupload(args):
4895 return CMDupload(OptionParser(), args)
4896
4897 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004898 Changelist, WrappedCMDupload, options.dry_run,
4899 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004900
4901
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004902@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004903@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004904def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004905 """DEPRECATED: Used to commit the current changelist via git-svn."""
4906 message = ('git-cl no longer supports committing to SVN repositories via '
4907 'git-svn. You probably want to use `git cl land` instead.')
4908 print(message)
4909 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004910
4911
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004912# Two special branches used by git cl land.
4913MERGE_BRANCH = 'git-cl-commit'
4914CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4915
4916
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004917@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004918@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004919def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004920 """Commits the current changelist via git.
4921
4922 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4923 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004924 """
4925 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4926 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004927 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004928 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004929 parser.add_option('--parallel', action='store_true',
4930 help='Run all tests specified by input_api.RunTests in all '
4931 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004932 auth.add_auth_options(parser)
4933 (options, args) = parser.parse_args(args)
4934 auth_config = auth.extract_auth_config_from_options(options)
4935
4936 cl = Changelist(auth_config=auth_config)
4937
Robert Iannucci2e73d432018-03-14 01:10:47 -07004938 if not cl.IsGerrit():
4939 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004940
Robert Iannucci2e73d432018-03-14 01:10:47 -07004941 if not cl.GetIssue():
4942 DieWithError('You must upload the change first to Gerrit.\n'
4943 ' If you would rather have `git cl land` upload '
4944 'automatically for you, see http://crbug.com/642759')
4945 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004946 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004947
4948
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004949@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004950@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004951def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004952 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004953 parser.add_option('-b', dest='newbranch',
4954 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004955 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004956 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004957 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004958 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004959 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004960 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004961 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004962 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004963 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004964 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004965
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004966
4967 group = optparse.OptionGroup(
4968 parser,
4969 'Options for continuing work on the current issue uploaded from a '
4970 'different clone (e.g. different machine). Must be used independently '
4971 'from the other options. No issue number should be specified, and the '
4972 'branch must have an issue number associated with it')
4973 group.add_option('--reapply', action='store_true', dest='reapply',
4974 help='Reset the branch and reapply the issue.\n'
4975 'CAUTION: This will undo any local changes in this '
4976 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004977
4978 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004979 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004980 parser.add_option_group(group)
4981
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004982 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004983 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004984 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004985 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004986 auth_config = auth.extract_auth_config_from_options(options)
4987
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004988 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004989 if options.newbranch:
4990 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004991 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004992 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004993
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004994 cl = Changelist(auth_config=auth_config,
4995 codereview=options.forced_codereview)
4996 if not cl.GetIssue():
4997 parser.error('current branch must have an associated issue')
4998
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004999 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005000 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005001 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005002
5003 RunGit(['reset', '--hard', upstream])
5004 if options.pull:
5005 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005006
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005007 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5008 options.directory)
5009
5010 if len(args) != 1 or not args[0]:
5011 parser.error('Must specify issue number or url')
5012
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005013 target_issue_arg = ParseIssueNumberArgument(args[0],
5014 options.forced_codereview)
5015 if not target_issue_arg.valid:
5016 parser.error('invalid codereview url or CL id')
5017
5018 cl_kwargs = {
5019 'auth_config': auth_config,
5020 'codereview_host': target_issue_arg.hostname,
5021 'codereview': options.forced_codereview,
5022 }
5023 detected_codereview_from_url = False
5024 if target_issue_arg.codereview and not options.forced_codereview:
5025 detected_codereview_from_url = True
5026 cl_kwargs['codereview'] = target_issue_arg.codereview
5027 cl_kwargs['issue'] = target_issue_arg.issue
5028
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005029 # We don't want uncommitted changes mixed up with the patch.
5030 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005031 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005032
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005033 if options.newbranch:
5034 if options.force:
5035 RunGit(['branch', '-D', options.newbranch],
5036 stderr=subprocess2.PIPE, error_ok=True)
5037 RunGit(['new-branch', options.newbranch])
5038
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005039 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005040
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005041 if cl.IsGerrit():
5042 if options.reject:
5043 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005044 if options.directory:
5045 parser.error('--directory is not supported with Gerrit codereview.')
5046
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005047 if detected_codereview_from_url:
5048 print('canonical issue/change URL: %s (type: %s)\n' %
5049 (cl.GetIssueURL(), target_issue_arg.codereview))
5050
5051 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005052 options.nocommit, options.directory,
5053 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005054
5055
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005056def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005057 """Fetches the tree status and returns either 'open', 'closed',
5058 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005059 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005060 if url:
5061 status = urllib2.urlopen(url).read().lower()
5062 if status.find('closed') != -1 or status == '0':
5063 return 'closed'
5064 elif status.find('open') != -1 or status == '1':
5065 return 'open'
5066 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005067 return 'unset'
5068
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005070def GetTreeStatusReason():
5071 """Fetches the tree status from a json url and returns the message
5072 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005073 url = settings.GetTreeStatusUrl()
5074 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005075 connection = urllib2.urlopen(json_url)
5076 status = json.loads(connection.read())
5077 connection.close()
5078 return status['message']
5079
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005080
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005081@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005082def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005083 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005084 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005085 status = GetTreeStatus()
5086 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005087 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005088 return 2
5089
vapiera7fbd5a2016-06-16 09:17:49 -07005090 print('The tree is %s' % status)
5091 print()
5092 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005093 if status != 'open':
5094 return 1
5095 return 0
5096
5097
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005098@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005099def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005100 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005101 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005102 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005103 '-b', '--bot', action='append',
5104 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5105 'times to specify multiple builders. ex: '
5106 '"-b win_rel -b win_layout". See '
5107 'the try server waterfall for the builders name and the tests '
5108 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005109 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005110 '-B', '--bucket', default='',
5111 help=('Buildbucket bucket to send the try requests.'))
5112 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005113 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005114 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005115 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005116 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005117 help='Revision to use for the try job; default: the revision will '
5118 'be determined by the try recipe that builder runs, which usually '
5119 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005120 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005121 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005122 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005123 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005124 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005125 '--category', default='git_cl_try', help='Specify custom build category.')
5126 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005127 '--project',
5128 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005129 'in recipe to determine to which repository or directory to '
5130 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005131 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005132 '-p', '--property', dest='properties', action='append', default=[],
5133 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005134 'key2=value2 etc. The value will be treated as '
5135 'json if decodable, or as string otherwise. '
5136 'NOTE: using this may make your try job not usable for CQ, '
5137 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005138 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005139 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5140 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005141 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005142 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005143 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005144 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005145 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005146 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005147
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005148 if options.master and options.master.startswith('luci.'):
5149 parser.error(
5150 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005151 # Make sure that all properties are prop=value pairs.
5152 bad_params = [x for x in options.properties if '=' not in x]
5153 if bad_params:
5154 parser.error('Got properties with missing "=": %s' % bad_params)
5155
maruel@chromium.org15192402012-09-06 12:38:29 +00005156 if args:
5157 parser.error('Unknown arguments: %s' % args)
5158
Koji Ishii31c14782018-01-08 17:17:33 +09005159 cl = Changelist(auth_config=auth_config, issue=options.issue,
5160 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005161 if not cl.GetIssue():
5162 parser.error('Need to upload first')
5163
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005164 if cl.IsGerrit():
5165 # HACK: warm up Gerrit change detail cache to save on RPCs.
5166 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5167
tandriie113dfd2016-10-11 10:20:12 -07005168 error_message = cl.CannotTriggerTryJobReason()
5169 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005170 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005171
borenet6c0efe62016-10-19 08:13:29 -07005172 if options.bucket and options.master:
5173 parser.error('Only one of --bucket and --master may be used.')
5174
qyearsley1fdfcb62016-10-24 13:22:03 -07005175 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005176
qyearsleydd49f942016-10-28 11:57:22 -07005177 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5178 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005179 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005180 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005181 print('git cl try with no bots now defaults to CQ dry run.')
5182 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5183 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005184
borenet6c0efe62016-10-19 08:13:29 -07005185 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005186 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005187 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005188 'of bot requires an initial job from a parent (usually a builder). '
5189 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005190 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005191 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005192
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005193 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005194 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005195 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005196 except BuildbucketResponseException as ex:
5197 print('ERROR: %s' % ex)
5198 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005199 return 0
5200
5201
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005202@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005203def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005204 """Prints info about try jobs associated with current CL."""
5205 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005206 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005207 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005208 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005209 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005210 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005211 '--color', action='store_true', default=setup_color.IS_TTY,
5212 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005213 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005214 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5215 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005216 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005217 '--json', help=('Path of JSON output file to write try job results to,'
5218 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005219 parser.add_option_group(group)
5220 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005221 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005222 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005223 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005224 if args:
5225 parser.error('Unrecognized args: %s' % ' '.join(args))
5226
5227 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005228 cl = Changelist(
5229 issue=options.issue, codereview=options.forced_codereview,
5230 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005231 if not cl.GetIssue():
5232 parser.error('Need to upload first')
5233
tandrii221ab252016-10-06 08:12:04 -07005234 patchset = options.patchset
5235 if not patchset:
5236 patchset = cl.GetMostRecentPatchset()
5237 if not patchset:
5238 parser.error('Codereview doesn\'t know about issue %s. '
5239 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005240 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005241 cl.GetIssue())
5242
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005243 try:
tandrii221ab252016-10-06 08:12:04 -07005244 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005245 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005246 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005247 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005248 if options.json:
5249 write_try_results_json(options.json, jobs)
5250 else:
5251 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005252 return 0
5253
5254
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005255@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005256@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005257def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005258 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005259 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005260 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005261 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005263 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005264 if args:
5265 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005266 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005267 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005268 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005269 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005270
5271 # Clear configured merge-base, if there is one.
5272 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005273 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005274 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005275 return 0
5276
5277
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005278@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005279def CMDweb(parser, args):
5280 """Opens the current CL in the web browser."""
5281 _, args = parser.parse_args(args)
5282 if args:
5283 parser.error('Unrecognized args: %s' % ' '.join(args))
5284
5285 issue_url = Changelist().GetIssueURL()
5286 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005287 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005288 return 1
5289
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005290 # Redirect I/O before invoking browser to hide its output. For example, this
5291 # allows to hide "Created new window in existing browser session." message
5292 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5293 saved_stdout = os.dup(1)
5294 os.close(1)
5295 os.open(os.devnull, os.O_RDWR)
5296 try:
5297 webbrowser.open(issue_url)
5298 finally:
5299 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005300 return 0
5301
5302
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005303@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005304def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005305 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005306 parser.add_option('-d', '--dry-run', action='store_true',
5307 help='trigger in dry run mode')
5308 parser.add_option('-c', '--clear', action='store_true',
5309 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005310 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005311 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005312 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005313 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005314 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005315 if args:
5316 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005317 if options.dry_run and options.clear:
5318 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5319
iannuccie53c9352016-08-17 14:40:40 -07005320 cl = Changelist(auth_config=auth_config, issue=options.issue,
5321 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005322 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005323 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005324 elif options.dry_run:
5325 state = _CQState.DRY_RUN
5326 else:
5327 state = _CQState.COMMIT
5328 if not cl.GetIssue():
5329 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005330 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005331 return 0
5332
5333
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005334@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005335def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005336 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005337 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005338 auth.add_auth_options(parser)
5339 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005340 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005341 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005342 if args:
5343 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005344 cl = Changelist(auth_config=auth_config, issue=options.issue,
5345 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005346 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005347 if not cl.GetIssue():
5348 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005349 cl.CloseIssue()
5350 return 0
5351
5352
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005353@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005354def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005355 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005356 parser.add_option(
5357 '--stat',
5358 action='store_true',
5359 dest='stat',
5360 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005361 auth.add_auth_options(parser)
5362 options, args = parser.parse_args(args)
5363 auth_config = auth.extract_auth_config_from_options(options)
5364 if args:
5365 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005366
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005367 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005368 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005369 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005370 if not issue:
5371 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005372
Aaron Gablea718c3e2017-08-28 17:47:28 -07005373 base = cl._GitGetBranchConfigValue('last-upload-hash')
5374 if not base:
5375 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5376 if not base:
5377 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5378 revision_info = detail['revisions'][detail['current_revision']]
5379 fetch_info = revision_info['fetch']['http']
5380 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5381 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005382
Aaron Gablea718c3e2017-08-28 17:47:28 -07005383 cmd = ['git', 'diff']
5384 if options.stat:
5385 cmd.append('--stat')
5386 cmd.append(base)
5387 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005388
5389 return 0
5390
5391
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005392@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005393def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005394 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005395 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005396 '--ignore-current',
5397 action='store_true',
5398 help='Ignore the CL\'s current reviewers and start from scratch.')
5399 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005400 '--no-color',
5401 action='store_true',
5402 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005403 parser.add_option(
5404 '--batch',
5405 action='store_true',
5406 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005407 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005408 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005409 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005410
5411 author = RunGit(['config', 'user.email']).strip() or None
5412
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005413 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005414
5415 if args:
5416 if len(args) > 1:
5417 parser.error('Unknown args')
5418 base_branch = args[0]
5419 else:
5420 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005421 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005422
5423 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005424 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5425
5426 if options.batch:
5427 db = owners.Database(change.RepositoryRoot(), file, os.path)
5428 print('\n'.join(db.reviewers_for(affected_files, author)))
5429 return 0
5430
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005431 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005432 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005433 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005434 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005435 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005436 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005437 disable_color=options.no_color,
5438 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005439
5440
Aiden Bennerc08566e2018-10-03 17:52:42 +00005441def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005442 """Generates a diff command."""
5443 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005444 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5445
5446 if not allow_prefix:
5447 diff_cmd += ['--no-prefix']
5448
5449 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005450
5451 if args:
5452 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005453 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005454 diff_cmd.append(arg)
5455 else:
5456 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005457
5458 return diff_cmd
5459
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005460
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005461def MatchingFileType(file_name, extensions):
5462 """Returns true if the file name ends with one of the given extensions."""
5463 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005464
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005465
enne@chromium.org555cfe42014-01-29 18:21:39 +00005466@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005467@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005468def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005469 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005470 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005471 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005472 parser.add_option('--full', action='store_true',
5473 help='Reformat the full content of all touched files')
5474 parser.add_option('--dry-run', action='store_true',
5475 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005476 parser.add_option(
5477 '--python',
5478 action='store_true',
5479 default=None,
5480 help='Enables python formatting on all python files.')
5481 parser.add_option(
5482 '--no-python',
5483 action='store_true',
5484 dest='python',
5485 help='Disables python formatting on all python files. '
5486 'Takes precedence over --python. '
5487 'If neither --python or --no-python are set, python '
5488 'files that have a .style.yapf file in an ancestor '
5489 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005490 parser.add_option('--js', action='store_true',
5491 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005492 parser.add_option('--diff', action='store_true',
5493 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005494 parser.add_option('--presubmit', action='store_true',
5495 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005496 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005497
Daniel Chengc55eecf2016-12-30 03:11:02 -08005498 # Normalize any remaining args against the current path, so paths relative to
5499 # the current directory are still resolved as expected.
5500 args = [os.path.join(os.getcwd(), arg) for arg in args]
5501
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005502 # git diff generates paths against the root of the repository. Change
5503 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005504 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005505 if rel_base_path:
5506 os.chdir(rel_base_path)
5507
digit@chromium.org29e47272013-05-17 17:01:46 +00005508 # Grab the merge-base commit, i.e. the upstream commit of the current
5509 # branch when it was created or the last time it was rebased. This is
5510 # to cover the case where the user may have called "git fetch origin",
5511 # moving the origin branch to a newer commit, but hasn't rebased yet.
5512 upstream_commit = None
5513 cl = Changelist()
5514 upstream_branch = cl.GetUpstreamBranch()
5515 if upstream_branch:
5516 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5517 upstream_commit = upstream_commit.strip()
5518
5519 if not upstream_commit:
5520 DieWithError('Could not find base commit for this branch. '
5521 'Are you in detached state?')
5522
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005523 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5524 diff_output = RunGit(changed_files_cmd)
5525 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005526 # Filter out files deleted by this CL
5527 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005528
Christopher Lamc5ba6922017-01-24 11:19:14 +11005529 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005530 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005531
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005532 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5533 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5534 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005535 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005536
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005537 top_dir = os.path.normpath(
5538 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5539
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005540 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5541 # formatted. This is used to block during the presubmit.
5542 return_value = 0
5543
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005544 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005545 # Locate the clang-format binary in the checkout
5546 try:
5547 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005548 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005549 DieWithError(e)
5550
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005551 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005552 cmd = [clang_format_tool]
5553 if not opts.dry_run and not opts.diff:
5554 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005555 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005556 if opts.diff:
5557 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005558 else:
5559 env = os.environ.copy()
5560 env['PATH'] = str(os.path.dirname(clang_format_tool))
5561 try:
5562 script = clang_format.FindClangFormatScriptInChromiumTree(
5563 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005564 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005565 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005566
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005567 cmd = [sys.executable, script, '-p0']
5568 if not opts.dry_run and not opts.diff:
5569 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005570
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005571 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5572 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005573
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005574 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5575 if opts.diff:
5576 sys.stdout.write(stdout)
5577 if opts.dry_run and len(stdout) > 0:
5578 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005579
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005580 # Similar code to above, but using yapf on .py files rather than clang-format
5581 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005582 py_explicitly_disabled = opts.python is not None and not opts.python
5583 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005584 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5585 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5586 if sys.platform.startswith('win'):
5587 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005588
Aiden Bennerc08566e2018-10-03 17:52:42 +00005589 # If we couldn't find a yapf file we'll default to the chromium style
5590 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005591 chromium_default_yapf_style = os.path.join(depot_tools_path,
5592 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005593 # Used for caching.
5594 yapf_configs = {}
5595 for f in python_diff_files:
5596 # Find the yapf style config for the current file, defaults to depot
5597 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005598 _FindYapfConfigFile(f, yapf_configs, top_dir)
5599
5600 # Turn on python formatting by default if a yapf config is specified.
5601 # This breaks in the case of this repo though since the specified
5602 # style file is also the global default.
5603 if opts.python is None:
5604 filtered_py_files = []
5605 for f in python_diff_files:
5606 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5607 filtered_py_files.append(f)
5608 else:
5609 filtered_py_files = python_diff_files
5610
5611 # Note: yapf still seems to fix indentation of the entire file
5612 # even if line ranges are specified.
5613 # See https://github.com/google/yapf/issues/499
5614 if not opts.full and filtered_py_files:
5615 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5616
5617 for f in filtered_py_files:
5618 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5619 if yapf_config is None:
5620 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005621
5622 cmd = [yapf_tool, '--style', yapf_config, f]
5623
5624 has_formattable_lines = False
5625 if not opts.full:
5626 # Only run yapf over changed line ranges.
5627 for diff_start, diff_len in py_line_diffs[f]:
5628 diff_end = diff_start + diff_len - 1
5629 # Yapf errors out if diff_end < diff_start but this
5630 # is a valid line range diff for a removal.
5631 if diff_end >= diff_start:
5632 has_formattable_lines = True
5633 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5634 # If all line diffs were removals we have nothing to format.
5635 if not has_formattable_lines:
5636 continue
5637
5638 if opts.diff or opts.dry_run:
5639 cmd += ['--diff']
5640 # Will return non-zero exit code if non-empty diff.
5641 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5642 if opts.diff:
5643 sys.stdout.write(stdout)
5644 elif len(stdout) > 0:
5645 return_value = 2
5646 else:
5647 cmd += ['-i']
5648 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005649
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005650 # Dart's formatter does not have the nice property of only operating on
5651 # modified chunks, so hard code full.
5652 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005653 try:
5654 command = [dart_format.FindDartFmtToolInChromiumTree()]
5655 if not opts.dry_run and not opts.diff:
5656 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005657 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005658
ppi@chromium.org6593d932016-03-03 15:41:15 +00005659 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005660 if opts.dry_run and stdout:
5661 return_value = 2
5662 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005663 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5664 'found in this checkout. Files in other languages are still '
5665 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005666
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005667 # Format GN build files. Always run on full build files for canonical form.
5668 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005669 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005670 if opts.dry_run or opts.diff:
5671 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005672 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005673 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5674 shell=sys.platform == 'win32',
5675 cwd=top_dir)
5676 if opts.dry_run and gn_ret == 2:
5677 return_value = 2 # Not formatted.
5678 elif opts.diff and gn_ret == 2:
5679 # TODO this should compute and print the actual diff.
5680 print("This change has GN build file diff for " + gn_diff_file)
5681 elif gn_ret != 0:
5682 # For non-dry run cases (and non-2 return values for dry-run), a
5683 # nonzero error code indicates a failure, probably because the file
5684 # doesn't parse.
5685 DieWithError("gn format failed on " + gn_diff_file +
5686 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005687
Ilya Shermane081cbe2017-08-15 17:51:04 -07005688 # Skip the metrics formatting from the global presubmit hook. These files have
5689 # a separate presubmit hook that issues an error if the files need formatting,
5690 # whereas the top-level presubmit script merely issues a warning. Formatting
5691 # these files is somewhat slow, so it's important not to duplicate the work.
5692 if not opts.presubmit:
5693 for xml_dir in GetDirtyMetricsDirs(diff_files):
5694 tool_dir = os.path.join(top_dir, xml_dir)
5695 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5696 if opts.dry_run or opts.diff:
5697 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005698 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005699 if opts.diff:
5700 sys.stdout.write(stdout)
5701 if opts.dry_run and stdout:
5702 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005703
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005704 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005705
Steven Holte2e664bf2017-04-21 13:10:47 -07005706def GetDirtyMetricsDirs(diff_files):
5707 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5708 metrics_xml_dirs = [
5709 os.path.join('tools', 'metrics', 'actions'),
5710 os.path.join('tools', 'metrics', 'histograms'),
5711 os.path.join('tools', 'metrics', 'rappor'),
5712 os.path.join('tools', 'metrics', 'ukm')]
5713 for xml_dir in metrics_xml_dirs:
5714 if any(file.startswith(xml_dir) for file in xml_diff_files):
5715 yield xml_dir
5716
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005717
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005718@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005719@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005720def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005721 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005722 _, args = parser.parse_args(args)
5723
5724 if len(args) != 1:
5725 parser.print_help()
5726 return 1
5727
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005728 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005729 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005730 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005731
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005732 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005733
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005734 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005735 output = RunGit(['config', '--local', '--get-regexp',
5736 r'branch\..*\.%s' % issueprefix],
5737 error_ok=True)
5738 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005739 if issue == target_issue:
5740 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005741
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005742 branches = []
5743 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005744 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005745 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005746 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005747 return 1
5748 if len(branches) == 1:
5749 RunGit(['checkout', branches[0]])
5750 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005751 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005752 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005753 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005754 which = raw_input('Choose by index: ')
5755 try:
5756 RunGit(['checkout', branches[int(which)]])
5757 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005758 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005759 return 1
5760
5761 return 0
5762
5763
maruel@chromium.org29404b52014-09-08 22:58:00 +00005764def CMDlol(parser, args):
5765 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005766 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005767 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5768 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5769 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005770 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005771 return 0
5772
5773
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005774class OptionParser(optparse.OptionParser):
5775 """Creates the option parse and add --verbose support."""
5776 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005777 optparse.OptionParser.__init__(
5778 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005779 self.add_option(
5780 '-v', '--verbose', action='count', default=0,
5781 help='Use 2 times for more debugging info')
5782
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005783 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005784 try:
5785 return self._parse_args(args)
5786 finally:
5787 # Regardless of success or failure of args parsing, we want to report
5788 # metrics, but only after logging has been initialized (if parsing
5789 # succeeded).
5790 global settings
5791 settings = Settings()
5792
5793 if not metrics.DISABLE_METRICS_COLLECTION:
5794 # GetViewVCUrl ultimately calls logging method.
5795 project_url = settings.GetViewVCUrl().strip('/+')
5796 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5797 metrics.collector.add('project_urls', [project_url])
5798
5799 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005800 # Create an optparse.Values object that will store only the actual passed
5801 # options, without the defaults.
5802 actual_options = optparse.Values()
5803 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5804 # Create an optparse.Values object with the default options.
5805 options = optparse.Values(self.get_default_values().__dict__)
5806 # Update it with the options passed by the user.
5807 options._update_careful(actual_options.__dict__)
5808 # Store the options passed by the user in an _actual_options attribute.
5809 # We store only the keys, and not the values, since the values can contain
5810 # arbitrary information, which might be PII.
5811 metrics.collector.add('arguments', actual_options.__dict__.keys())
5812
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005813 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005814 logging.basicConfig(
5815 level=levels[min(options.verbose, len(levels) - 1)],
5816 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5817 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005818
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005819 return options, args
5820
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005821
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005822def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005823 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005824 print('\nYour python version %s is unsupported, please upgrade.\n' %
5825 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005826 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005827
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005828 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005829 dispatcher = subcommand.CommandDispatcher(__name__)
5830 try:
5831 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005832 except auth.AuthenticationError as e:
5833 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005834 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005835 if e.code != 500:
5836 raise
5837 DieWithError(
5838 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5839 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005840 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005841
5842
5843if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005844 # These affect sys.stdout so do it outside of main() to simplify mocks in
5845 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005846 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005847 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005848 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005849 sys.exit(main(sys.argv[1:]))