blob: 1a817c522908167f8b8dcbbd8d0b31bdb0b803ce [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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and 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
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
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
piman@chromium.org336f9122014-09-04 02:16:55 +000058import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000059import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000061import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040063import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066import watchlists
67
tandrii7400cf02016-06-21 08:48:07 -070068__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069
tandrii9d2c7a32016-06-22 03:42:45 -070070COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070071DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
borenet6c0efe62016-10-19 08:13:29 -070083# Buildbucket master name prefix.
84MASTER_PREFIX = 'master.'
85
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000086# Shortcut since it quickly becomes redundant.
87Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000088
maruel@chromium.orgddd59412011-11-30 14:20:38 +000089# Initialized in main()
90settings = None
91
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010092# Used by tests/git_cl_test.py to add extra logging.
93# Inside the weirdly failing test, add this:
94# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070095# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096_IS_BEING_TESTED = False
97
maruel@chromium.orgddd59412011-11-30 14:20:38 +000098
Christopher Lamf732cd52017-01-24 12:40:11 +110099def DieWithError(message, change_desc=None):
100 if change_desc:
101 SaveDescriptionBackup(change_desc)
102
vapiera7fbd5a2016-06-16 09:17:49 -0700103 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 sys.exit(1)
105
106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def SaveDescriptionBackup(change_desc):
108 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
109 print('\nError after CL description prompt -- saving description to %s\n' %
110 backup_path)
111 backup_file = open(backup_path, 'w')
112 backup_file.write(change_desc.description)
113 backup_file.close()
114
115
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000116def GetNoGitPagerEnv():
117 env = os.environ.copy()
118 # 'cat' is a magical git string that disables pagers on all platforms.
119 env['GIT_PAGER'] = 'cat'
120 return env
121
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000122
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 'Command "%s" failed.\n%s' % (
131 ' '.join(args), error_message or e.stdout or ''))
132 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
135def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000137 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000140def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700142 if suppress_stderr:
143 stderr = subprocess2.VOID
144 else:
145 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000146 try:
tandrii5d48c322016-08-18 16:19:37 -0700147 (out, _), code = subprocess2.communicate(['git'] + args,
148 env=GetNoGitPagerEnv(),
149 stdout=subprocess2.PIPE,
150 stderr=stderr)
151 return code, out
152 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900153 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700154 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155
156
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000158 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159 return RunGitWithCode(args, suppress_stderr=True)[1]
160
161
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 return (version.startswith(prefix) and
166 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167
168
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000169def BranchExists(branch):
170 """Return True if specified branch exists."""
171 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
172 suppress_stderr=True)
173 return not code
174
175
tandrii2a16b952016-10-19 07:09:44 -0700176def time_sleep(seconds):
177 # Use this so that it can be mocked in tests without interfering with python
178 # system machinery.
179 import time # Local import to discourage others from importing time globally.
180 return time.sleep(seconds)
181
182
maruel@chromium.org90541732011-04-01 17:54:18 +0000183def ask_for_data(prompt):
184 try:
185 return raw_input(prompt)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100191def confirm_or_exit(prefix='', action='confirm'):
192 """Asks user to press enter to continue or press Ctrl+C to abort."""
193 if not prefix or prefix.endswith('\n'):
194 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100195 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100196 mid = ' Press'
197 elif prefix.endswith(' '):
198 mid = 'press'
199 else:
200 mid = ' press'
201 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
202
203
204def ask_for_explicit_yes(prompt):
205 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
206 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
207 while True:
208 if 'yes'.startswith(result):
209 return True
210 if 'no'.startswith(result):
211 return False
212 result = ask_for_data('Please, type yes or no: ').lower()
213
214
tandrii5d48c322016-08-18 16:19:37 -0700215def _git_branch_config_key(branch, key):
216 """Helper method to return Git config key for a branch."""
217 assert branch, 'branch name is required to set git config for it'
218 return 'branch.%s.%s' % (branch, key)
219
220
221def _git_get_branch_config_value(key, default=None, value_type=str,
222 branch=False):
223 """Returns git config value of given or current branch if any.
224
225 Returns default in all other cases.
226 """
227 assert value_type in (int, str, bool)
228 if branch is False: # Distinguishing default arg value from None.
229 branch = GetCurrentBranch()
230
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700232 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000233
tandrii5d48c322016-08-18 16:19:37 -0700234 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700235 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700237 # git config also has --int, but apparently git config suffers from integer
238 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append(_git_branch_config_key(branch, key))
240 code, out = RunGitWithCode(args)
241 if code == 0:
242 value = out.strip()
243 if value_type == int:
244 return int(value)
245 if value_type == bool:
246 return bool(value.lower() == 'true')
247 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 return default
249
250
tandrii5d48c322016-08-18 16:19:37 -0700251def _git_set_branch_config_value(key, value, branch=None, **kwargs):
252 """Sets the value or unsets if it's None of a git branch config.
253
254 Valid, though not necessarily existing, branch must be provided,
255 otherwise currently checked out branch is used.
256 """
257 if not branch:
258 branch = GetCurrentBranch()
259 assert branch, 'a branch name OR currently checked out branch is required'
260 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700261 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700262 if value is None:
263 args.append('--unset')
264 elif isinstance(value, bool):
265 args.append('--bool')
266 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700267 else:
tandrii33a46ff2016-08-23 05:53:40 -0700268 # git config also has --int, but apparently git config suffers from integer
269 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700270 value = str(value)
271 args.append(_git_branch_config_key(branch, key))
272 if value is not None:
273 args.append(value)
274 RunGit(args, **kwargs)
275
276
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100277def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700278 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100279
280 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
281 """
282 # Git also stores timezone offset, but it only affects visual display,
283 # actual point in time is defined by this timestamp only.
284 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
285
286
287def _git_amend_head(message, committer_timestamp):
288 """Amends commit with new message and desired committer_timestamp.
289
290 Sets committer timezone to UTC.
291 """
292 env = os.environ.copy()
293 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
294 return RunGit(['commit', '--amend', '-m', message], env=env)
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
354 if not content_json:
355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
357 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
358 content)
359 return content_json
360 if response.status < 500 or try_count >= 2:
361 raise httplib2.HttpLib2Error(content)
362
363 # status >= 500 means transient failures.
364 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700365 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 try_count += 1
367 assert False, 'unreachable'
368
369
qyearsley1fdfcb62016-10-24 13:22:03 -0700370def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700371 """Returns a dict mapping bucket names to builders and tests,
372 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373 """
qyearsleydd49f942016-10-28 11:57:22 -0700374 # If no bots are listed, we try to get a set of builders and tests based
375 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 if not options.bot:
377 change = changelist.GetChange(
378 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700379 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700380 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 change=change,
382 changed_files=change.LocalPaths(),
383 repository_root=settings.GetRoot(),
384 default_presubmit=None,
385 project=None,
386 verbose=options.verbose,
387 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700388 if masters is None:
389 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100390 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 if options.bucket:
393 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700394 if options.master:
395 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
qyearsleydd49f942016-10-28 11:57:22 -0700397 # If bots are listed but no master or bucket, then we need to find out
398 # the corresponding master for each bot.
399 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
400 if error_message:
401 option_parser.error(
402 'Tryserver master cannot be found because: %s\n'
403 'Please manually specify the tryserver master, e.g. '
404 '"-m tryserver.chromium.linux".' % error_message)
405 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
qyearsley123a4682016-10-26 09:12:17 -0700408def _get_bucket_map_for_builders(builders):
409 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 map_url = 'https://builders-map.appspot.com/'
411 try:
qyearsley123a4682016-10-26 09:12:17 -0700412 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 except urllib2.URLError as e:
414 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
415 (map_url, e))
416 except ValueError as e:
417 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700418 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 return None, 'Failed to build master map.'
420
qyearsley123a4682016-10-26 09:12:17 -0700421 bucket_map = {}
422 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800423 bucket = builders_map.get(builder, {}).get('bucket')
424 if bucket:
425 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700426 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700427
428
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800429def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 """Sends a request to Buildbucket to trigger try jobs for a changelist.
431
432 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700433 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700434 changelist: Changelist that the try jobs are associated with.
435 buckets: A nested dict mapping bucket names to builders to tests.
436 options: Command-line options.
437 """
tandriide281ae2016-10-12 06:02:30 -0700438 assert changelist.GetIssue(), 'CL must be uploaded first'
439 codereview_url = changelist.GetCodereviewServer()
440 assert codereview_url, 'CL must be uploaded first'
441 patchset = patchset or changelist.GetMostRecentPatchset()
442 assert patchset, 'CL must be uploaded first'
443
444 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700445 # Cache the buildbucket credentials under the codereview host key, so that
446 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700447 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000448 http = authenticator.authorize(httplib2.Http())
449 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700450
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000451 buildbucket_put_url = (
452 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000453 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700454 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
455 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
456 hostname=codereview_host,
457 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700459
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700460 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800461 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700462 if options.clobber:
463 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700464 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700465 if extra_properties:
466 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000467
468 batch_req_body = {'builds': []}
469 print_text = []
470 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700471 for bucket, builders_and_tests in sorted(buckets.iteritems()):
472 print_text.append('Bucket: %s' % bucket)
473 master = None
474 if bucket.startswith(MASTER_PREFIX):
475 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000476 for builder, tests in sorted(builders_and_tests.iteritems()):
477 print_text.append(' %s: %s' % (builder, tests))
478 parameters = {
479 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000480 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100481 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000482 'revision': options.revision,
483 }],
tandrii8c5a3532016-11-04 07:52:02 -0700484 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000486 if 'presubmit' in builder.lower():
487 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000488 if tests:
489 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700490
491 tags = [
492 'builder:%s' % builder,
493 'buildset:%s' % buildset,
494 'user_agent:git_cl_try',
495 ]
496 if master:
497 parameters['properties']['master'] = master
498 tags.append('master:%s' % master)
499
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 batch_req_body['builds'].append(
501 {
502 'bucket': bucket,
503 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700505 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 }
507 )
508
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700510 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 http,
512 buildbucket_put_url,
513 'PUT',
514 body=json.dumps(batch_req_body),
515 headers={'Content-Type': 'application/json'}
516 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000517 print_text.append('To see results here, run: git cl try-results')
518 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700519 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000520
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521
tandrii221ab252016-10-06 08:12:04 -0700522def fetch_try_jobs(auth_config, changelist, buildbucket_host,
523 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700524 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525
qyearsley53f48a12016-09-01 10:45:13 -0700526 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000527 """
tandrii221ab252016-10-06 08:12:04 -0700528 assert buildbucket_host
529 assert changelist.GetIssue(), 'CL must be uploaded first'
530 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
531 patchset = patchset or changelist.GetMostRecentPatchset()
532 assert patchset, 'CL must be uploaded first'
533
534 codereview_url = changelist.GetCodereviewServer()
535 codereview_host = urlparse.urlparse(codereview_url).hostname
536 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 if authenticator.has_cached_credentials():
538 http = authenticator.authorize(httplib2.Http())
539 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700540 print('Warning: Some results might be missing because %s' %
541 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700542 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 http = httplib2.Http()
544
545 http.force_exception_to_status_code = True
546
tandrii221ab252016-10-06 08:12:04 -0700547 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
548 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
549 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700551 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 params = {'tag': 'buildset:%s' % buildset}
553
554 builds = {}
555 while True:
556 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700557 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700559 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 for build in content.get('builds', []):
561 builds[build['id']] = build
562 if 'next_cursor' in content:
563 params['start_cursor'] = content['next_cursor']
564 else:
565 break
566 return builds
567
568
qyearsleyeab3c042016-08-24 09:18:28 -0700569def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 """Prints nicely result of fetch_try_jobs."""
571 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700572 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 return
574
575 # Make a copy, because we'll be modifying builds dictionary.
576 builds = builds.copy()
577 builder_names_cache = {}
578
579 def get_builder(b):
580 try:
581 return builder_names_cache[b['id']]
582 except KeyError:
583 try:
584 parameters = json.loads(b['parameters_json'])
585 name = parameters['builder_name']
586 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700587 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700588 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000589 name = None
590 builder_names_cache[b['id']] = name
591 return name
592
593 def get_bucket(b):
594 bucket = b['bucket']
595 if bucket.startswith('master.'):
596 return bucket[len('master.'):]
597 return bucket
598
599 if options.print_master:
600 name_fmt = '%%-%ds %%-%ds' % (
601 max(len(str(get_bucket(b))) for b in builds.itervalues()),
602 max(len(str(get_builder(b))) for b in builds.itervalues()))
603 def get_name(b):
604 return name_fmt % (get_bucket(b), get_builder(b))
605 else:
606 name_fmt = '%%-%ds' % (
607 max(len(str(get_builder(b))) for b in builds.itervalues()))
608 def get_name(b):
609 return name_fmt % get_builder(b)
610
611 def sort_key(b):
612 return b['status'], b.get('result'), get_name(b), b.get('url')
613
614 def pop(title, f, color=None, **kwargs):
615 """Pop matching builds from `builds` dict and print them."""
616
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000617 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 colorize = str
619 else:
620 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
621
622 result = []
623 for b in builds.values():
624 if all(b.get(k) == v for k, v in kwargs.iteritems()):
625 builds.pop(b['id'])
626 result.append(b)
627 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700628 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700630 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631
632 total = len(builds)
633 pop(status='COMPLETED', result='SUCCESS',
634 title='Successes:', color=Fore.GREEN,
635 f=lambda b: (get_name(b), b.get('url')))
636 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
637 title='Infra Failures:', color=Fore.MAGENTA,
638 f=lambda b: (get_name(b), b.get('url')))
639 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
640 title='Failures:', color=Fore.RED,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='CANCELED',
643 title='Canceled:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b),))
645 pop(status='COMPLETED', result='FAILURE',
646 failure_reason='INVALID_BUILD_DEFINITION',
647 title='Wrong master/builder name:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b),))
649 pop(status='COMPLETED', result='FAILURE',
650 title='Other failures:',
651 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
652 pop(status='COMPLETED',
653 title='Other finished:',
654 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
655 pop(status='STARTED',
656 title='Started:', color=Fore.YELLOW,
657 f=lambda b: (get_name(b), b.get('url')))
658 pop(status='SCHEDULED',
659 title='Scheduled:',
660 f=lambda b: (get_name(b), 'id=%s' % b['id']))
661 # The last section is just in case buildbucket API changes OR there is a bug.
662 pop(title='Other:',
663 f=lambda b: (get_name(b), 'id=%s' % b['id']))
664 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700665 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666
667
qyearsley53f48a12016-09-01 10:45:13 -0700668def write_try_results_json(output_file, builds):
669 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
670
671 The input |builds| dict is assumed to be generated by Buildbucket.
672 Buildbucket documentation: http://goo.gl/G0s101
673 """
674
675 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800676 """Extracts some of the information from one build dict."""
677 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700678 return {
679 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700680 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800681 'builder_name': parameters.get('builder_name'),
682 'created_ts': build.get('created_ts'),
683 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700684 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800685 'result': build.get('result'),
686 'status': build.get('status'),
687 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700688 'url': build.get('url'),
689 }
690
691 converted = []
692 for _, build in sorted(builds.items()):
693 converted.append(convert_build_dict(build))
694 write_json(output_file, converted)
695
696
Aaron Gable13101a62018-02-09 13:20:41 -0800697def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000698 """Prints statistics about the change to the user."""
699 # --no-ext-diff is broken in some versions of Git, so try to work around
700 # this by overriding the environment (but there is still a problem if the
701 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000702 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000703 if 'GIT_EXTERNAL_DIFF' in env:
704 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000705
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000706 try:
707 stdout = sys.stdout.fileno()
708 except AttributeError:
709 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000710 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800711 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000712 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000713
714
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000715class BuildbucketResponseException(Exception):
716 pass
717
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719class Settings(object):
720 def __init__(self):
721 self.default_server = None
722 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000723 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 self.tree_status_url = None
725 self.viewvc_url = None
726 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000727 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000728 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000729 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000730 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000731 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000732 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
734 def LazyUpdateIfNeeded(self):
735 """Updates the settings from a codereview.settings file, if available."""
736 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000737 # The only value that actually changes the behavior is
738 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000739 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000740 error_ok=True
741 ).strip().lower()
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000744 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 LoadCodereviewSettingsFromFile(cr_settings_file)
746 self.updated = True
747
748 def GetDefaultServerUrl(self, error_ok=False):
749 if not self.default_server:
750 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000751 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000752 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 if error_ok:
754 return self.default_server
755 if not self.default_server:
756 error_message = ('Could not find settings file. You must configure '
757 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000758 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 return self.default_server
761
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000762 @staticmethod
763 def GetRelativeRoot():
764 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000767 if self.root is None:
768 self.root = os.path.abspath(self.GetRelativeRoot())
769 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000771 def GetGitMirror(self, remote='origin'):
772 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000773 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000774 if not os.path.isdir(local_url):
775 return None
776 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
777 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100778 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100779 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000780 if mirror.exists():
781 return mirror
782 return None
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 def GetTreeStatusUrl(self, error_ok=False):
785 if not self.tree_status_url:
786 error_message = ('You must configure your tree status URL by running '
787 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 self.tree_status_url = self._GetRietveldConfig(
789 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return self.tree_status_url
791
792 def GetViewVCUrl(self):
793 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 return self.viewvc_url
796
rmistry@google.com90752582014-01-14 21:04:50 +0000797 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000798 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000799
rmistry@google.com78948ed2015-07-08 23:09:57 +0000800 def GetIsSkipDependencyUpload(self, branch_name):
801 """Returns true if specified branch should skip dep uploads."""
802 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
803 error_ok=True)
804
rmistry@google.com5626a922015-02-26 14:03:30 +0000805 def GetRunPostUploadHook(self):
806 run_post_upload_hook = self._GetRietveldConfig(
807 'run-post-upload-hook', error_ok=True)
808 return run_post_upload_hook == "True"
809
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000810 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000812
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000814 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000815
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700817 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000818 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700819 self.is_gerrit = (
820 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000821 return self.is_gerrit
822
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000823 def GetSquashGerritUploads(self):
824 """Return true if uploads to Gerrit should be squashed by default."""
825 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700826 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
827 if self.squash_gerrit_uploads is None:
828 # Default is squash now (http://crbug.com/611892#c23).
829 self.squash_gerrit_uploads = not (
830 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
831 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000832 return self.squash_gerrit_uploads
833
tandriia60502f2016-06-20 02:01:53 -0700834 def GetSquashGerritUploadsOverride(self):
835 """Return True or False if codereview.settings should be overridden.
836
837 Returns None if no override has been defined.
838 """
839 # See also http://crbug.com/611892#c23
840 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
841 error_ok=True).strip()
842 if result == 'true':
843 return True
844 if result == 'false':
845 return False
846 return None
847
tandrii@chromium.org28253532016-04-14 13:46:56 +0000848 def GetGerritSkipEnsureAuthenticated(self):
849 """Return True if EnsureAuthenticated should not be done for Gerrit
850 uploads."""
851 if self.gerrit_skip_ensure_authenticated is None:
852 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000853 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000854 error_ok=True).strip() == 'true')
855 return self.gerrit_skip_ensure_authenticated
856
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000857 def GetGitEditor(self):
858 """Return the editor specified in the git config, or None if none is."""
859 if self.git_editor is None:
860 self.git_editor = self._GetConfig('core.editor', error_ok=True)
861 return self.git_editor or None
862
thestig@chromium.org44202a22014-03-11 19:22:18 +0000863 def GetLintRegex(self):
864 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
865 DEFAULT_LINT_REGEX)
866
867 def GetLintIgnoreRegex(self):
868 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
869 DEFAULT_LINT_IGNORE_REGEX)
870
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000871 def GetProject(self):
872 if not self.project:
873 self.project = self._GetRietveldConfig('project', error_ok=True)
874 return self.project
875
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000876 def _GetRietveldConfig(self, param, **kwargs):
877 return self._GetConfig('rietveld.' + param, **kwargs)
878
rmistry@google.com78948ed2015-07-08 23:09:57 +0000879 def _GetBranchConfig(self, branch_name, param, **kwargs):
880 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 def _GetConfig(self, param, **kwargs):
883 self.LazyUpdateIfNeeded()
884 return RunGit(['config', param], **kwargs).strip()
885
886
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100887@contextlib.contextmanager
888def _get_gerrit_project_config_file(remote_url):
889 """Context manager to fetch and store Gerrit's project.config from
890 refs/meta/config branch and store it in temp file.
891
892 Provides a temporary filename or None if there was error.
893 """
894 error, _ = RunGitWithCode([
895 'fetch', remote_url,
896 '+refs/meta/config:refs/git_cl/meta/config'])
897 if error:
898 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700899 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100900 (remote_url, error))
901 yield None
902 return
903
904 error, project_config_data = RunGitWithCode(
905 ['show', 'refs/git_cl/meta/config:project.config'])
906 if error:
907 print('WARNING: project.config file not found')
908 yield None
909 return
910
911 with gclient_utils.temporary_directory() as tempdir:
912 project_config_file = os.path.join(tempdir, 'project.config')
913 gclient_utils.FileWrite(project_config_file, project_config_data)
914 yield project_config_file
915
916
917def _is_git_numberer_enabled(remote_url, remote_ref):
918 """Returns True if Git Numberer is enabled on this ref."""
919 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100920 KNOWN_PROJECTS_WHITELIST = [
921 'chromium/src',
922 'external/webrtc',
923 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100924 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200925 # For webrtc.googlesource.com/src.
926 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100927 ]
928
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100929 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
930 url_parts = urlparse.urlparse(remote_url)
931 project_name = url_parts.path.lstrip('/').rstrip('git./')
932 for known in KNOWN_PROJECTS_WHITELIST:
933 if project_name.endswith(known):
934 break
935 else:
936 # Early exit to avoid extra fetches for repos that aren't using Git
937 # Numberer.
938 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100939
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100940 with _get_gerrit_project_config_file(remote_url) as project_config_file:
941 if project_config_file is None:
942 # Failed to fetch project.config, which shouldn't happen on open source
943 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100944 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100945 def get_opts(x):
946 code, out = RunGitWithCode(
947 ['config', '-f', project_config_file, '--get-all',
948 'plugin.git-numberer.validate-%s-refglob' % x])
949 if code == 0:
950 return out.strip().splitlines()
951 return []
952 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100953
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100954 logging.info('validator config enabled %s disabled %s refglobs for '
955 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000956
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100957 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100958 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100959 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100960 return True
961 return False
962
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100963 if match_refglobs(disabled):
964 return False
965 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966
967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968def ShortBranchName(branch):
969 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970 return branch.replace('refs/heads/', '', 1)
971
972
973def GetCurrentBranchRef():
974 """Returns branch ref (e.g., refs/heads/master) or None."""
975 return RunGit(['symbolic-ref', 'HEAD'],
976 stderr=subprocess2.VOID, error_ok=True).strip() or None
977
978
979def GetCurrentBranch():
980 """Returns current branch or None.
981
982 For refs/heads/* branches, returns just last part. For others, full ref.
983 """
984 branchref = GetCurrentBranchRef()
985 if branchref:
986 return ShortBranchName(branchref)
987 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988
989
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000990class _CQState(object):
991 """Enum for states of CL with respect to Commit Queue."""
992 NONE = 'none'
993 DRY_RUN = 'dry_run'
994 COMMIT = 'commit'
995
996 ALL_STATES = [NONE, DRY_RUN, COMMIT]
997
998
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001000 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001001 self.issue = issue
1002 self.patchset = patchset
1003 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001004 assert codereview in (None, 'rietveld', 'gerrit')
1005 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001006
1007 @property
1008 def valid(self):
1009 return self.issue is not None
1010
1011
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001012def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001013 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1014 fail_result = _ParsedIssueNumberArgument()
1015
1016 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001017 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001018 if not arg.startswith('http'):
1019 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001020
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001021 url = gclient_utils.UpgradeToHttps(arg)
1022 try:
1023 parsed_url = urlparse.urlparse(url)
1024 except ValueError:
1025 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001026
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001027 if codereview is not None:
1028 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1029 return parsed or fail_result
1030
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001031 results = {}
1032 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1033 parsed = cls.ParseIssueURL(parsed_url)
1034 if parsed is not None:
1035 results[name] = parsed
1036
1037 if not results:
1038 return fail_result
1039 if len(results) == 1:
1040 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001041
1042 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1043 # This is likely Gerrit.
1044 return results['gerrit']
1045 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001046 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001047
1048
Aaron Gablea45ee112016-11-22 15:14:38 -08001049class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001050 def __init__(self, issue, url):
1051 self.issue = issue
1052 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001053 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001054
1055 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001056 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001057 self.issue, self.url)
1058
1059
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001060_CommentSummary = collections.namedtuple(
1061 '_CommentSummary', ['date', 'message', 'sender',
1062 # TODO(tandrii): these two aren't known in Gerrit.
1063 'approval', 'disapproval'])
1064
1065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001067 """Changelist works with one changelist in local branch.
1068
1069 Supports two codereview backends: Rietveld or Gerrit, selected at object
1070 creation.
1071
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001072 Notes:
1073 * Not safe for concurrent multi-{thread,process} use.
1074 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001075 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 """
1077
1078 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1079 """Create a new ChangeList instance.
1080
1081 If issue is given, the codereview must be given too.
1082
1083 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1084 Otherwise, it's decided based on current configuration of the local branch,
1085 with default being 'rietveld' for backwards compatibility.
1086 See _load_codereview_impl for more details.
1087
1088 **kwargs will be passed directly to codereview implementation.
1089 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001091 global settings
1092 if not settings:
1093 # Happens when git_cl.py is used as a utility library.
1094 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001095
1096 if issue:
1097 assert codereview, 'codereview must be known, if issue is known'
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.branchref = branchref
1100 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001101 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102 self.branch = ShortBranchName(self.branchref)
1103 else:
1104 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001106 self.lookedup_issue = False
1107 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 self.has_description = False
1109 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001110 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001112 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001113 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001118 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001119 assert self._codereview_impl
1120 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121
1122 def _load_codereview_impl(self, codereview=None, **kwargs):
1123 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001124 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1125 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1126 self._codereview = codereview
1127 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128 return
1129
1130 # Automatic selection based on issue number set for a current branch.
1131 # Rietveld takes precedence over Gerrit.
1132 assert not self.issue
1133 # Whether we find issue or not, we are doing the lookup.
1134 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001135 if self.GetBranch():
1136 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1137 issue = _git_get_branch_config_value(
1138 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1139 if issue:
1140 self._codereview = codereview
1141 self._codereview_impl = cls(self, **kwargs)
1142 self.issue = int(issue)
1143 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144
1145 # No issue is set for this branch, so decide based on repo-wide settings.
1146 return self._load_codereview_impl(
1147 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1148 **kwargs)
1149
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001150 def IsGerrit(self):
1151 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001152
1153 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001154 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001155
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001156 The return value is a string suitable for passing to git cl with the --cc
1157 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 """
1159 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001161 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001162 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1163 return self.cc
1164
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001165 def GetCCListWithoutDefault(self):
1166 """Return the users cc'd on this CL excluding default ones."""
1167 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001168 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001169 return self.cc
1170
Daniel Cheng7227d212017-11-17 08:12:37 -08001171 def ExtendCC(self, more_cc):
1172 """Extends the list of users to cc on this CL based on the changed files."""
1173 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
1175 def GetBranch(self):
1176 """Returns the short branch name, e.g. 'master'."""
1177 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001179 if not branchref:
1180 return None
1181 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 self.branch = ShortBranchName(self.branchref)
1183 return self.branch
1184
1185 def GetBranchRef(self):
1186 """Returns the full branch name, e.g. 'refs/heads/master'."""
1187 self.GetBranch() # Poke the lazy loader.
1188 return self.branchref
1189
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001190 def ClearBranch(self):
1191 """Clears cached branch data of this object."""
1192 self.branch = self.branchref = None
1193
tandrii5d48c322016-08-18 16:19:37 -07001194 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1195 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1196 kwargs['branch'] = self.GetBranch()
1197 return _git_get_branch_config_value(key, default, **kwargs)
1198
1199 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1200 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1201 assert self.GetBranch(), (
1202 'this CL must have an associated branch to %sset %s%s' %
1203 ('un' if value is None else '',
1204 key,
1205 '' if value is None else ' to %r' % value))
1206 kwargs['branch'] = self.GetBranch()
1207 return _git_set_branch_config_value(key, value, **kwargs)
1208
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001209 @staticmethod
1210 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001211 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 e.g. 'origin', 'refs/heads/master'
1213 """
1214 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001215 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001218 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001220 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1221 error_ok=True).strip()
1222 if upstream_branch:
1223 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001225 # Else, try to guess the origin remote.
1226 remote_branches = RunGit(['branch', '-r']).split()
1227 if 'origin/master' in remote_branches:
1228 # Fall back on origin/master if it exits.
1229 remote = 'origin'
1230 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001232 DieWithError(
1233 'Unable to determine default branch to diff against.\n'
1234 'Either pass complete "git diff"-style arguments, like\n'
1235 ' git cl upload origin/master\n'
1236 'or verify this branch is set up to track another \n'
1237 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238
1239 return remote, upstream_branch
1240
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001241 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001242 upstream_branch = self.GetUpstreamBranch()
1243 if not BranchExists(upstream_branch):
1244 DieWithError('The upstream for the current branch (%s) does not exist '
1245 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001246 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001247 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 def GetUpstreamBranch(self):
1250 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001253 upstream_branch = upstream_branch.replace('refs/heads/',
1254 'refs/remotes/%s/' % remote)
1255 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1256 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 self.upstream_branch = upstream_branch
1258 return self.upstream_branch
1259
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001260 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001261 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, branch = None, self.GetBranch()
1263 seen_branches = set()
1264 while branch not in seen_branches:
1265 seen_branches.add(branch)
1266 remote, branch = self.FetchUpstreamTuple(branch)
1267 branch = ShortBranchName(branch)
1268 if remote != '.' or branch.startswith('refs/remotes'):
1269 break
1270 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001271 remotes = RunGit(['remote'], error_ok=True).split()
1272 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001274 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001276 logging.warn('Could not determine which remote this change is '
1277 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001278 else:
1279 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001280 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 branch = 'HEAD'
1282 if branch.startswith('refs/remotes'):
1283 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001284 elif branch.startswith('refs/branch-heads/'):
1285 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 else:
1287 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001288 return self._remote
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 def GitSanityChecks(self, upstream_git_obj):
1291 """Checks git repo status and ensures diff is from local commits."""
1292
sbc@chromium.org79706062015-01-14 21:18:12 +00001293 if upstream_git_obj is None:
1294 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001295 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001296 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001297 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001298 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001299 return False
1300
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 # Verify the commit we're diffing against is in our current branch.
1302 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1303 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1304 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001305 print('ERROR: %s is not in the current branch. You may need to rebase '
1306 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001307 return False
1308
1309 # List the commits inside the diff, and verify they are all local.
1310 commits_in_diff = RunGit(
1311 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1312 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1313 remote_branch = remote_branch.strip()
1314 if code != 0:
1315 _, remote_branch = self.GetRemoteBranch()
1316
1317 commits_in_remote = RunGit(
1318 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1319
1320 common_commits = set(commits_in_diff) & set(commits_in_remote)
1321 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001322 print('ERROR: Your diff contains %d commits already in %s.\n'
1323 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1324 'the diff. If you are using a custom git flow, you can override'
1325 ' the reference used for this check with "git config '
1326 'gitcl.remotebranch <git-ref>".' % (
1327 len(common_commits), remote_branch, upstream_git_obj),
1328 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 return False
1330 return True
1331
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001332 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001333 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001334
1335 Returns None if it is not set.
1336 """
tandrii5d48c322016-08-18 16:19:37 -07001337 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 def GetRemoteUrl(self):
1340 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1341
1342 Returns None if there is no remote.
1343 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001345 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1346
1347 # If URL is pointing to a local directory, it is probably a git cache.
1348 if os.path.isdir(url):
1349 url = RunGit(['config', 'remote.%s.url' % remote],
1350 error_ok=True,
1351 cwd=url).strip()
1352 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001354 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001355 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001356 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001357 self.issue = self._GitGetBranchConfigValue(
1358 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 return self.issue
1361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 def GetIssueURL(self):
1363 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001364 issue = self.GetIssue()
1365 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001366 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001367 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001369 def GetDescription(self, pretty=False, force=False):
1370 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001372 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 self.has_description = True
1374 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001375 # Set width to 72 columns + 2 space indent.
1376 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001378 lines = self.description.splitlines()
1379 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 return self.description
1381
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001382 def GetDescriptionFooters(self):
1383 """Returns (non_footer_lines, footers) for the commit message.
1384
1385 Returns:
1386 non_footer_lines (list(str)) - Simple list of description lines without
1387 any footer. The lines do not contain newlines, nor does the list contain
1388 the empty line between the message and the footers.
1389 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1390 [("Change-Id", "Ideadbeef...."), ...]
1391 """
1392 raw_description = self.GetDescription()
1393 msg_lines, _, footers = git_footers.split_footers(raw_description)
1394 if footers:
1395 msg_lines = msg_lines[:len(msg_lines)-1]
1396 return msg_lines, footers
1397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001399 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001401 self.patchset = self._GitGetBranchConfigValue(
1402 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001403 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 return self.patchset
1405
1406 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001407 """Set this branch's patchset. If patchset=0, clears the patchset."""
1408 assert self.GetBranch()
1409 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001410 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001411 else:
1412 self.patchset = int(patchset)
1413 self._GitSetBranchConfigValue(
1414 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001416 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001417 """Set this branch's issue. If issue isn't given, clears the issue."""
1418 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001420 issue = int(issue)
1421 self._GitSetBranchConfigValue(
1422 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001423 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 codereview_server = self._codereview_impl.GetCodereviewServer()
1425 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001426 self._GitSetBranchConfigValue(
1427 self._codereview_impl.CodereviewServerConfigKey(),
1428 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 else:
tandrii5d48c322016-08-18 16:19:37 -07001430 # Reset all of these just to be clean.
1431 reset_suffixes = [
1432 'last-upload-hash',
1433 self._codereview_impl.IssueConfigKey(),
1434 self._codereview_impl.PatchsetConfigKey(),
1435 self._codereview_impl.CodereviewServerConfigKey(),
1436 ] + self._PostUnsetIssueProperties()
1437 for prop in reset_suffixes:
1438 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001439 msg = RunGit(['log', '-1', '--format=%B']).strip()
1440 if msg and git_footers.get_footer_change_id(msg):
1441 print('WARNING: The change patched into this branch has a Change-Id. '
1442 'Removing it.')
1443 RunGit(['commit', '--amend', '-m',
1444 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001446 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447
dnjba1b0f32016-09-02 12:37:42 -07001448 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 if not self.GitSanityChecks(upstream_branch):
1450 DieWithError('\nGit sanity check failure')
1451
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001452 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001453 if not root:
1454 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001456
1457 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001459 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001461 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001462 except subprocess2.CalledProcessError:
1463 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001464 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 'This branch probably doesn\'t exist anymore. To reset the\n'
1466 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001467 ' git branch --set-upstream-to origin/master %s\n'
1468 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470
maruel@chromium.org52424302012-08-29 15:14:30 +00001471 issue = self.GetIssue()
1472 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001473 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001474 description = self.GetDescription()
1475 else:
1476 # If the change was never uploaded, use the log messages of all commits
1477 # up to the branch point, as git cl upload will prefill the description
1478 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1480 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001481
1482 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001483 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001484 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001485 name,
1486 description,
1487 absroot,
1488 files,
1489 issue,
1490 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001491 author,
1492 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493
dsansomee2d6fd92016-09-08 00:10:47 -07001494 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001497 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001499 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1500 """Sets the description for this CL remotely.
1501
1502 You can get description_lines and footers with GetDescriptionFooters.
1503
1504 Args:
1505 description_lines (list(str)) - List of CL description lines without
1506 newline characters.
1507 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1508 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1509 `List-Of-Tokens`). It will be case-normalized so that each token is
1510 title-cased.
1511 """
1512 new_description = '\n'.join(description_lines)
1513 if footers:
1514 new_description += '\n'
1515 for k, v in footers:
1516 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1517 if not git_footers.FOOTER_PATTERN.match(foot):
1518 raise ValueError('Invalid footer %r' % foot)
1519 new_description += foot + '\n'
1520 self.UpdateDescription(new_description, force)
1521
Edward Lesmes8e282792018-04-03 18:50:29 -04001522 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1524 try:
1525 return presubmit_support.DoPresubmitChecks(change, committing,
1526 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1527 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001528 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1529 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001530 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001531 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001533 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1534 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001535 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1536 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001537 else:
1538 # Assume url.
1539 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1540 urlparse.urlparse(issue_arg))
1541 if not parsed_issue_arg or not parsed_issue_arg.valid:
1542 DieWithError('Failed to parse issue argument "%s". '
1543 'Must be an issue number or a valid URL.' % issue_arg)
1544 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001545 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001546
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 def CMDUpload(self, options, git_diff_args, orig_args):
1548 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001549 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001551 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001552 else:
1553 if self.GetBranch() is None:
1554 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1555
1556 # Default to diffing against common ancestor of upstream branch
1557 base_branch = self.GetCommonAncestorWithUpstream()
1558 git_diff_args = [base_branch, 'HEAD']
1559
Aaron Gablec4c40d12017-05-22 11:49:53 -07001560 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1561 if not self.IsGerrit() and not self.GetIssue():
1562 print('=====================================')
1563 print('NOTICE: Rietveld is being deprecated. '
1564 'You can upload changes to Gerrit with')
1565 print(' git cl upload --gerrit')
1566 print('or set Gerrit to be your default code review tool with')
1567 print(' git config gerrit.host true')
1568 print('=====================================')
1569
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001570 # Fast best-effort checks to abort before running potentially
1571 # expensive hooks if uploading is likely to fail anyway. Passing these
1572 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001573 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001574 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575
1576 # Apply watchlists on upload.
1577 change = self.GetChange(base_branch, None)
1578 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1579 files = [f.LocalPath() for f in change.AffectedFiles()]
1580 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001581 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582
1583 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001584 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 # Set the reviewer list now so that presubmit checks can access it.
1586 change_description = ChangeDescription(change.FullDescriptionText())
1587 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001588 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001589 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 change)
1591 change.SetDescriptionText(change_description.description)
1592 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001593 may_prompt=not options.force,
1594 verbose=options.verbose,
1595 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596 if not hook_results.should_continue():
1597 return 1
1598 if not options.reviewers and hook_results.reviewers:
1599 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001600 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001602 # TODO(tandrii): Checking local patchset against remote patchset is only
1603 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1604 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001605 latest_patchset = self.GetMostRecentPatchset()
1606 local_patchset = self.GetPatchset()
1607 if (latest_patchset and local_patchset and
1608 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001609 print('The last upload made from this repository was patchset #%d but '
1610 'the most recent patchset on the server is #%d.'
1611 % (local_patchset, latest_patchset))
1612 print('Uploading will still work, but if you\'ve uploaded to this '
1613 'issue from another machine or branch the patch you\'re '
1614 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001615 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616
Aaron Gable13101a62018-02-09 13:20:41 -08001617 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001618 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001619 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001620 if self.IsGerrit():
1621 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1622 options.cq_dry_run);
1623 else:
1624 if options.use_commit_queue:
1625 self.SetCQState(_CQState.COMMIT)
1626 elif options.cq_dry_run:
1627 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001628
tandrii5d48c322016-08-18 16:19:37 -07001629 _git_set_branch_config_value('last-upload-hash',
1630 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631 # Run post upload hooks, if specified.
1632 if settings.GetRunPostUploadHook():
1633 presubmit_support.DoPostUploadExecuter(
1634 change,
1635 self,
1636 settings.GetRoot(),
1637 options.verbose,
1638 sys.stdout)
1639
1640 # Upload all dependencies if specified.
1641 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001642 print()
1643 print('--dependencies has been specified.')
1644 print('All dependent local branches will be re-uploaded.')
1645 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 # Remove the dependencies flag from args so that we do not end up in a
1647 # loop.
1648 orig_args.remove('--dependencies')
1649 ret = upload_branch_deps(self, orig_args)
1650 return ret
1651
Ravi Mistry31e7d562018-04-02 12:53:57 -04001652 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1653 """Sets labels on the change based on the provided flags.
1654
1655 Sets labels if issue is already uploaded and known, else returns without
1656 doing anything.
1657
1658 Args:
1659 enable_auto_submit: Sets Auto-Submit+1 on the change.
1660 use_commit_queue: Sets Commit-Queue+2 on the change.
1661 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1662 both use_commit_queue and cq_dry_run are true.
1663 """
1664 if not self.GetIssue():
1665 return
1666 try:
1667 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1668 cq_dry_run)
1669 return 0
1670 except KeyboardInterrupt:
1671 raise
1672 except:
1673 labels = []
1674 if enable_auto_submit:
1675 labels.append('Auto-Submit')
1676 if use_commit_queue or cq_dry_run:
1677 labels.append('Commit-Queue')
1678 print('WARNING: Failed to set label(s) on your change: %s\n'
1679 'Either:\n'
1680 ' * Your project does not have the above label(s),\n'
1681 ' * You don\'t have permission to set the above label(s),\n'
1682 ' * There\'s a bug in this code (see stack trace below).\n' %
1683 (', '.join(labels)))
1684 # Still raise exception so that stack trace is printed.
1685 raise
1686
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001687 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001688 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001689
1690 Issue must have been already uploaded and known.
1691 """
1692 assert new_state in _CQState.ALL_STATES
1693 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001694 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001695 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001696 return 0
1697 except KeyboardInterrupt:
1698 raise
1699 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001700 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001701 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001702 ' * Your project has no CQ,\n'
1703 ' * You don\'t have permission to change the CQ state,\n'
1704 ' * There\'s a bug in this code (see stack trace below).\n'
1705 'Consider specifying which bots to trigger manually or asking your '
1706 'project owners for permissions or contacting Chrome Infra at:\n'
1707 'https://www.chromium.org/infra\n\n' %
1708 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001709 # Still raise exception so that stack trace is printed.
1710 raise
1711
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712 # Forward methods to codereview specific implementation.
1713
Aaron Gable636b13f2017-07-14 10:42:48 -07001714 def AddComment(self, message, publish=None):
1715 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001716
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001717 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001718 """Returns list of _CommentSummary for each comment.
1719
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001720 args:
1721 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001722 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001723 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001724
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 def CloseIssue(self):
1726 return self._codereview_impl.CloseIssue()
1727
1728 def GetStatus(self):
1729 return self._codereview_impl.GetStatus()
1730
1731 def GetCodereviewServer(self):
1732 return self._codereview_impl.GetCodereviewServer()
1733
tandriide281ae2016-10-12 06:02:30 -07001734 def GetIssueOwner(self):
1735 """Get owner from codereview, which may differ from this checkout."""
1736 return self._codereview_impl.GetIssueOwner()
1737
Edward Lemur707d70b2018-02-07 00:50:14 +01001738 def GetReviewers(self):
1739 return self._codereview_impl.GetReviewers()
1740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetMostRecentPatchset(self):
1742 return self._codereview_impl.GetMostRecentPatchset()
1743
tandriide281ae2016-10-12 06:02:30 -07001744 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001745 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001746 return self._codereview_impl.CannotTriggerTryJobReason()
1747
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001748 def GetTryJobProperties(self, patchset=None):
1749 """Returns dictionary of properties to launch try job."""
1750 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def __getattr__(self, attr):
1753 # This is because lots of untested code accesses Rietveld-specific stuff
1754 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001755 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001756 # Note that child method defines __getattr__ as well, and forwards it here,
1757 # because _RietveldChangelistImpl is not cleaned up yet, and given
1758 # deprecation of Rietveld, it should probably be just removed.
1759 # Until that time, avoid infinite recursion by bypassing __getattr__
1760 # of implementation class.
1761 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762
1763
1764class _ChangelistCodereviewBase(object):
1765 """Abstract base class encapsulating codereview specifics of a changelist."""
1766 def __init__(self, changelist):
1767 self._changelist = changelist # instance of Changelist
1768
1769 def __getattr__(self, attr):
1770 # Forward methods to changelist.
1771 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1772 # _RietveldChangelistImpl to avoid this hack?
1773 return getattr(self._changelist, attr)
1774
1775 def GetStatus(self):
1776 """Apply a rough heuristic to give a simple summary of an issue's review
1777 or CQ status, assuming adherence to a common workflow.
1778
1779 Returns None if no issue for this branch, or specific string keywords.
1780 """
1781 raise NotImplementedError()
1782
1783 def GetCodereviewServer(self):
1784 """Returns server URL without end slash, like "https://codereview.com"."""
1785 raise NotImplementedError()
1786
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001787 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 """Fetches and returns description from the codereview server."""
1789 raise NotImplementedError()
1790
tandrii5d48c322016-08-18 16:19:37 -07001791 @classmethod
1792 def IssueConfigKey(cls):
1793 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 raise NotImplementedError()
1795
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001796 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001797 def PatchsetConfigKey(cls):
1798 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 raise NotImplementedError()
1800
tandrii5d48c322016-08-18 16:19:37 -07001801 @classmethod
1802 def CodereviewServerConfigKey(cls):
1803 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 raise NotImplementedError()
1805
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001806 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001807 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001808 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001809
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001810 def GetGerritObjForPresubmit(self):
1811 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1812 return None
1813
dsansomee2d6fd92016-09-08 00:10:47 -07001814 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 """Update the description on codereview site."""
1816 raise NotImplementedError()
1817
Aaron Gable636b13f2017-07-14 10:42:48 -07001818 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001819 """Posts a comment to the codereview site."""
1820 raise NotImplementedError()
1821
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001822 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001823 raise NotImplementedError()
1824
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 def CloseIssue(self):
1826 """Closes the issue."""
1827 raise NotImplementedError()
1828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829 def GetMostRecentPatchset(self):
1830 """Returns the most recent patchset number from the codereview site."""
1831 raise NotImplementedError()
1832
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001833 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001834 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001835 """Fetches and applies the issue.
1836
1837 Arguments:
1838 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1839 reject: if True, reject the failed patch instead of switching to 3-way
1840 merge. Rietveld only.
1841 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1842 only.
1843 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001844 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001845 """
1846 raise NotImplementedError()
1847
1848 @staticmethod
1849 def ParseIssueURL(parsed_url):
1850 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1851 failed."""
1852 raise NotImplementedError()
1853
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001854 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001855 """Best effort check that user is authenticated with codereview server.
1856
1857 Arguments:
1858 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001859 refresh: whether to attempt to refresh credentials. Ignored if not
1860 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001861 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862 raise NotImplementedError()
1863
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001864 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001865 """Best effort check that uploading isn't supposed to fail for predictable
1866 reasons.
1867
1868 This method should raise informative exception if uploading shouldn't
1869 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001870
1871 Arguments:
1872 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001873 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001874 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001875
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001876 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001877 """Uploads a change to codereview."""
1878 raise NotImplementedError()
1879
Ravi Mistry31e7d562018-04-02 12:53:57 -04001880 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1881 """Sets labels on the change based on the provided flags.
1882
1883 Issue must have been already uploaded and known.
1884 """
1885 raise NotImplementedError()
1886
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001887 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001888 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001889
1890 Issue must have been already uploaded and known.
1891 """
1892 raise NotImplementedError()
1893
tandriie113dfd2016-10-11 10:20:12 -07001894 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001895 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001896 raise NotImplementedError()
1897
tandriide281ae2016-10-12 06:02:30 -07001898 def GetIssueOwner(self):
1899 raise NotImplementedError()
1900
Edward Lemur707d70b2018-02-07 00:50:14 +01001901 def GetReviewers(self):
1902 raise NotImplementedError()
1903
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001904 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001905 raise NotImplementedError()
1906
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001907
1908class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001909
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001910 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001911 super(_RietveldChangelistImpl, self).__init__(changelist)
1912 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001913 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001914 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001915
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001916 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001917 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918 self._props = None
1919 self._rpc_server = None
1920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001921 def GetCodereviewServer(self):
1922 if not self._rietveld_server:
1923 # If we're on a branch then get the server potentially associated
1924 # with that branch.
1925 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001926 self._rietveld_server = gclient_utils.UpgradeToHttps(
1927 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 if not self._rietveld_server:
1929 self._rietveld_server = settings.GetDefaultServerUrl()
1930 return self._rietveld_server
1931
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001932 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001933 """Best effort check that user is authenticated with Rietveld server."""
1934 if self._auth_config.use_oauth2:
1935 authenticator = auth.get_authenticator_for_host(
1936 self.GetCodereviewServer(), self._auth_config)
1937 if not authenticator.has_cached_credentials():
1938 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001939 if refresh:
1940 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001941
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001942 def EnsureCanUploadPatchset(self, force):
1943 # No checks for Rietveld because we are deprecating Rietveld.
1944 pass
1945
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001946 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 issue = self.GetIssue()
1948 assert issue
1949 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001950 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951 except urllib2.HTTPError as e:
1952 if e.code == 404:
1953 DieWithError(
1954 ('\nWhile fetching the description for issue %d, received a '
1955 '404 (not found)\n'
1956 'error. It is likely that you deleted this '
1957 'issue on the server. If this is the\n'
1958 'case, please run\n\n'
1959 ' git cl issue 0\n\n'
1960 'to clear the association with the deleted issue. Then run '
1961 'this command again.') % issue)
1962 else:
1963 DieWithError(
1964 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1965 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001966 print('Warning: Failed to retrieve CL description due to network '
1967 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001968 return ''
1969
1970 def GetMostRecentPatchset(self):
1971 return self.GetIssueProperties()['patchsets'][-1]
1972
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 def GetIssueProperties(self):
1974 if self._props is None:
1975 issue = self.GetIssue()
1976 if not issue:
1977 self._props = {}
1978 else:
1979 self._props = self.RpcServer().get_issue_properties(issue, True)
1980 return self._props
1981
tandriie113dfd2016-10-11 10:20:12 -07001982 def CannotTriggerTryJobReason(self):
1983 props = self.GetIssueProperties()
1984 if not props:
1985 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1986 if props.get('closed'):
1987 return 'CL %s is closed' % self.GetIssue()
1988 if props.get('private'):
1989 return 'CL %s is private' % self.GetIssue()
1990 return None
1991
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001992 def GetTryJobProperties(self, patchset=None):
1993 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001994 project = (self.GetIssueProperties() or {}).get('project')
1995 return {
1996 'issue': self.GetIssue(),
1997 'patch_project': project,
1998 'patch_storage': 'rietveld',
1999 'patchset': patchset or self.GetPatchset(),
2000 'rietveld': self.GetCodereviewServer(),
2001 }
2002
tandriide281ae2016-10-12 06:02:30 -07002003 def GetIssueOwner(self):
2004 return (self.GetIssueProperties() or {}).get('owner_email')
2005
Edward Lemur707d70b2018-02-07 00:50:14 +01002006 def GetReviewers(self):
2007 return (self.GetIssueProperties() or {}).get('reviewers')
2008
Aaron Gable636b13f2017-07-14 10:42:48 -07002009 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002010 return self.RpcServer().add_comment(self.GetIssue(), message)
2011
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002012 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002013 summary = []
2014 for message in self.GetIssueProperties().get('messages', []):
2015 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2016 summary.append(_CommentSummary(
2017 date=date,
2018 disapproval=bool(message['disapproval']),
2019 approval=bool(message['approval']),
2020 sender=message['sender'],
2021 message=message['text'],
2022 ))
2023 return summary
2024
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002025 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002026 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002027 or CQ status, assuming adherence to a common workflow.
2028
2029 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002030 * 'error' - error from review tool (including deleted issues)
2031 * 'unsent' - not sent for review
2032 * 'waiting' - waiting for review
2033 * 'reply' - waiting for owner to reply to review
2034 * 'not lgtm' - Code-Review label has been set negatively
2035 * 'lgtm' - LGTM from at least one approved reviewer
2036 * 'commit' - in the commit queue
2037 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002038 """
2039 if not self.GetIssue():
2040 return None
2041
2042 try:
2043 props = self.GetIssueProperties()
2044 except urllib2.HTTPError:
2045 return 'error'
2046
2047 if props.get('closed'):
2048 # Issue is closed.
2049 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002050 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002051 # Issue is in the commit queue.
2052 return 'commit'
2053
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002054 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002055 if not messages:
2056 # No message was sent.
2057 return 'unsent'
2058
2059 if get_approving_reviewers(props):
2060 return 'lgtm'
2061 elif get_approving_reviewers(props, disapproval=True):
2062 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002063
tandrii9d2c7a32016-06-22 03:42:45 -07002064 # Skip CQ messages that don't require owner's action.
2065 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2066 if 'Dry run:' in messages[-1]['text']:
2067 messages.pop()
2068 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2069 # This message always follows prior messages from CQ,
2070 # so skip this too.
2071 messages.pop()
2072 else:
2073 # This is probably a CQ messages warranting user attention.
2074 break
2075
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002076 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002077 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002078 return 'reply'
2079 return 'waiting'
2080
dsansomee2d6fd92016-09-08 00:10:47 -07002081 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002082 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002083
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002085 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002087 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002088 return self.SetFlags({flag: value})
2089
2090 def SetFlags(self, flags):
2091 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002092 """
phajdan.jr68598232016-08-10 03:28:28 -07002093 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002094 try:
tandrii4b233bd2016-07-06 03:50:29 -07002095 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002096 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002097 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002098 if e.code == 404:
2099 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2100 if e.code == 403:
2101 DieWithError(
2102 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002103 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002106 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107 """Returns an upload.RpcServer() to access this review's rietveld instance.
2108 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002109 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002110 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002112 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002113 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002115 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002116 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002117 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118
tandrii5d48c322016-08-18 16:19:37 -07002119 @classmethod
2120 def PatchsetConfigKey(cls):
2121 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122
tandrii5d48c322016-08-18 16:19:37 -07002123 @classmethod
2124 def CodereviewServerConfigKey(cls):
2125 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002126
Ravi Mistry31e7d562018-04-02 12:53:57 -04002127 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2128 raise NotImplementedError()
2129
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002130 def SetCQState(self, new_state):
2131 props = self.GetIssueProperties()
2132 if props.get('private'):
2133 DieWithError('Cannot set-commit on private issue')
2134
2135 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002136 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002137 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002138 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002139 else:
tandrii4b233bd2016-07-06 03:50:29 -07002140 assert new_state == _CQState.DRY_RUN
2141 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002142
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002143 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002144 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 # PatchIssue should never be called with a dirty tree. It is up to the
2146 # caller to check this, but just in case we assert here since the
2147 # consequences of the caller not checking this could be dire.
2148 assert(not git_common.is_dirty_git_tree('apply'))
2149 assert(parsed_issue_arg.valid)
2150 self._changelist.issue = parsed_issue_arg.issue
2151 if parsed_issue_arg.hostname:
2152 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2153
skobes6468b902016-10-24 08:45:10 -07002154 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2155 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2156 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157 try:
skobes6468b902016-10-24 08:45:10 -07002158 scm_obj.apply_patch(patchset_object)
2159 except Exception as e:
2160 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 return 1
2162
2163 # If we had an issue, commit the current state and register the issue.
2164 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002165 self.SetIssue(self.GetIssue())
2166 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2168 'patch from issue %(i)s at patchset '
2169 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2170 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002171 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002172 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002173 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002174 return 0
2175
2176 @staticmethod
2177 def ParseIssueURL(parsed_url):
2178 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2179 return None
wychen3c1c1722016-08-04 11:46:36 -07002180 # Rietveld patch: https://domain/<number>/#ps<patchset>
2181 match = re.match(r'/(\d+)/$', parsed_url.path)
2182 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2183 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002184 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002185 issue=int(match.group(1)),
2186 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002187 hostname=parsed_url.netloc,
2188 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002189 # Typical url: https://domain/<issue_number>[/[other]]
2190 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2191 if match:
skobes6468b902016-10-24 08:45:10 -07002192 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002193 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002194 hostname=parsed_url.netloc,
2195 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002196 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2197 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2198 if match:
skobes6468b902016-10-24 08:45:10 -07002199 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002200 issue=int(match.group(1)),
2201 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002202 hostname=parsed_url.netloc,
2203 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002204 return None
2205
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002206 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 """Upload the patch to Rietveld."""
2208 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2209 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2211 if options.emulate_svn_auto_props:
2212 upload_args.append('--emulate_svn_auto_props')
2213
2214 change_desc = None
2215
2216 if options.email is not None:
2217 upload_args.extend(['--email', options.email])
2218
2219 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002220 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002221 upload_args.extend(['--title', options.title])
2222 if options.message:
2223 upload_args.extend(['--message', options.message])
2224 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002225 print('This branch is associated with issue %s. '
2226 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 else:
nodirca166002016-06-27 10:59:51 -07002228 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002230 if options.message:
2231 message = options.message
2232 else:
2233 message = CreateDescriptionFromLog(args)
2234 if options.title:
2235 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002236 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002237 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002238 change_desc.update_reviewers(options.reviewers, options.tbrs,
2239 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002241 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242
2243 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002244 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002245 return 1
2246
2247 upload_args.extend(['--message', change_desc.description])
2248 if change_desc.get_reviewers():
2249 upload_args.append('--reviewers=%s' % ','.join(
2250 change_desc.get_reviewers()))
2251 if options.send_mail:
2252 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002253 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002254 upload_args.append('--send_mail')
2255
2256 # We check this before applying rietveld.private assuming that in
2257 # rietveld.cc only addresses which we can send private CLs to are listed
2258 # if rietveld.private is set, and so we should ignore rietveld.cc only
2259 # when --private is specified explicitly on the command line.
2260 if options.private:
2261 logging.warn('rietveld.cc is ignored since private flag is specified. '
2262 'You need to review and add them manually if necessary.')
2263 cc = self.GetCCListWithoutDefault()
2264 else:
2265 cc = self.GetCCList()
2266 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002267 if change_desc.get_cced():
2268 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002269 if cc:
2270 upload_args.extend(['--cc', cc])
2271
2272 if options.private or settings.GetDefaultPrivateFlag() == "True":
2273 upload_args.append('--private')
2274
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 # Include the upstream repo's URL in the change -- this is useful for
2276 # projects that have their source spread across multiple repos.
2277 remote_url = self.GetGitBaseUrlFromConfig()
2278 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002279 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2280 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2281 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002284 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 if target_ref:
2286 upload_args.extend(['--target_ref', target_ref])
2287
2288 # Look for dependent patchsets. See crbug.com/480453 for more details.
2289 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2290 upstream_branch = ShortBranchName(upstream_branch)
2291 if remote is '.':
2292 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002293 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002294 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002295 print()
2296 print('Skipping dependency patchset upload because git config '
2297 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2298 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 else:
2300 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002301 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 auth_config=auth_config)
2303 branch_cl_issue_url = branch_cl.GetIssueURL()
2304 branch_cl_issue = branch_cl.GetIssue()
2305 branch_cl_patchset = branch_cl.GetPatchset()
2306 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2307 upload_args.extend(
2308 ['--depends_on_patchset', '%s:%s' % (
2309 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002310 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 '\n'
2312 'The current branch (%s) is tracking a local branch (%s) with '
2313 'an associated CL.\n'
2314 'Adding %s/#ps%s as a dependency patchset.\n'
2315 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2316 branch_cl_patchset))
2317
2318 project = settings.GetProject()
2319 if project:
2320 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002321 else:
2322 print()
2323 print('WARNING: Uploading without a project specified. Please ensure '
2324 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2325 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002326
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002327 try:
2328 upload_args = ['upload'] + upload_args + args
2329 logging.info('upload.RealMain(%s)', upload_args)
2330 issue, patchset = upload.RealMain(upload_args)
2331 issue = int(issue)
2332 patchset = int(patchset)
2333 except KeyboardInterrupt:
2334 sys.exit(1)
2335 except:
2336 # If we got an exception after the user typed a description for their
2337 # change, back up the description before re-raising.
2338 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002339 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002340 raise
2341
2342 if not self.GetIssue():
2343 self.SetIssue(issue)
2344 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002345 return 0
2346
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002347
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002348class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002349 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002350 # auth_config is Rietveld thing, kept here to preserve interface only.
2351 super(_GerritChangelistImpl, self).__init__(changelist)
2352 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002353 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002354 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002355 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002356 # Map from change number (issue) to its detail cache.
2357 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002358
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002359 if codereview_host is not None:
2360 assert not codereview_host.startswith('https://'), codereview_host
2361 self._gerrit_host = codereview_host
2362 self._gerrit_server = 'https://%s' % codereview_host
2363
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002364 def _GetGerritHost(self):
2365 # Lazy load of configs.
2366 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002367 if self._gerrit_host and '.' not in self._gerrit_host:
2368 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2369 # This happens for internal stuff http://crbug.com/614312.
2370 parsed = urlparse.urlparse(self.GetRemoteUrl())
2371 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002372 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002373 ' Your current remote is: %s' % self.GetRemoteUrl())
2374 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2375 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002376 return self._gerrit_host
2377
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002378 def _GetGitHost(self):
2379 """Returns git host to be used when uploading change to Gerrit."""
2380 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2381
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002382 def GetCodereviewServer(self):
2383 if not self._gerrit_server:
2384 # If we're on a branch then get the server potentially associated
2385 # with that branch.
2386 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002387 self._gerrit_server = self._GitGetBranchConfigValue(
2388 self.CodereviewServerConfigKey())
2389 if self._gerrit_server:
2390 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002391 if not self._gerrit_server:
2392 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2393 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002394 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002395 parts[0] = parts[0] + '-review'
2396 self._gerrit_host = '.'.join(parts)
2397 self._gerrit_server = 'https://%s' % self._gerrit_host
2398 return self._gerrit_server
2399
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002400 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002401 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002402 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403
tandrii5d48c322016-08-18 16:19:37 -07002404 @classmethod
2405 def PatchsetConfigKey(cls):
2406 return 'gerritpatchset'
2407
2408 @classmethod
2409 def CodereviewServerConfigKey(cls):
2410 return 'gerritserver'
2411
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002412 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002413 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002414 if settings.GetGerritSkipEnsureAuthenticated():
2415 # For projects with unusual authentication schemes.
2416 # See http://crbug.com/603378.
2417 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002418 # Lazy-loader to identify Gerrit and Git hosts.
2419 if gerrit_util.GceAuthenticator.is_gce():
2420 return
2421 self.GetCodereviewServer()
2422 git_host = self._GetGitHost()
2423 assert self._gerrit_server and self._gerrit_host
2424 cookie_auth = gerrit_util.CookiesAuthenticator()
2425
2426 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2427 git_auth = cookie_auth.get_auth_header(git_host)
2428 if gerrit_auth and git_auth:
2429 if gerrit_auth == git_auth:
2430 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002431 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002432 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002433 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002434 ' %s\n'
2435 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002436 ' Consider running the following command:\n'
2437 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002438 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002439 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002440 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002441 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002442 cookie_auth.get_new_password_message(git_host)))
2443 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002444 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002445 return
2446 else:
2447 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002448 ([] if gerrit_auth else [self._gerrit_host]) +
2449 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002450 DieWithError('Credentials for the following hosts are required:\n'
2451 ' %s\n'
2452 'These are read from %s (or legacy %s)\n'
2453 '%s' % (
2454 '\n '.join(missing),
2455 cookie_auth.get_gitcookies_path(),
2456 cookie_auth.get_netrc_path(),
2457 cookie_auth.get_new_password_message(git_host)))
2458
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002459 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002460 if not self.GetIssue():
2461 return
2462
2463 # Warm change details cache now to avoid RPCs later, reducing latency for
2464 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002465 self._GetChangeDetail(
2466 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002467
2468 status = self._GetChangeDetail()['status']
2469 if status in ('MERGED', 'ABANDONED'):
2470 DieWithError('Change %s has been %s, new uploads are not allowed' %
2471 (self.GetIssueURL(),
2472 'submitted' if status == 'MERGED' else 'abandoned'))
2473
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002474 if gerrit_util.GceAuthenticator.is_gce():
2475 return
2476 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2477 self._GetGerritHost())
2478 if self.GetIssueOwner() == cookies_user:
2479 return
2480 logging.debug('change %s owner is %s, cookies user is %s',
2481 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002482 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002483 # so ask what Gerrit thinks of this user.
2484 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2485 if details['email'] == self.GetIssueOwner():
2486 return
2487 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002488 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002489 'as %s.\n'
2490 'Uploading may fail due to lack of permissions.' %
2491 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2492 confirm_or_exit(action='upload')
2493
2494
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002495 def _PostUnsetIssueProperties(self):
2496 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002497 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002498
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002499 def GetGerritObjForPresubmit(self):
2500 return presubmit_support.GerritAccessor(self._GetGerritHost())
2501
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002502 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002503 """Apply a rough heuristic to give a simple summary of an issue's review
2504 or CQ status, assuming adherence to a common workflow.
2505
2506 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002507 * 'error' - error from review tool (including deleted issues)
2508 * 'unsent' - no reviewers added
2509 * 'waiting' - waiting for review
2510 * 'reply' - waiting for uploader to reply to review
2511 * 'lgtm' - Code-Review label has been set
2512 * 'commit' - in the commit queue
2513 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002514 """
2515 if not self.GetIssue():
2516 return None
2517
2518 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002519 data = self._GetChangeDetail([
2520 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002521 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002522 return 'error'
2523
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002524 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002525 return 'closed'
2526
Aaron Gable9ab38c62017-04-06 14:36:33 -07002527 if data['labels'].get('Commit-Queue', {}).get('approved'):
2528 # The section will have an "approved" subsection if anyone has voted
2529 # the maximum value on the label.
2530 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002531
Aaron Gable9ab38c62017-04-06 14:36:33 -07002532 if data['labels'].get('Code-Review', {}).get('approved'):
2533 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002534
2535 if not data.get('reviewers', {}).get('REVIEWER', []):
2536 return 'unsent'
2537
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002538 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002539 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2540 last_message_author = messages.pop().get('author', {})
2541 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002542 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2543 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002544 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002545 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002546 if last_message_author.get('_account_id') == owner:
2547 # Most recent message was by owner.
2548 return 'waiting'
2549 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002550 # Some reply from non-owner.
2551 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002552
2553 # Somehow there are no messages even though there are reviewers.
2554 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002555
2556 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002557 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002558 patchset = data['revisions'][data['current_revision']]['_number']
2559 self.SetPatchset(patchset)
2560 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002561
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002562 def FetchDescription(self, force=False):
2563 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2564 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002565 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002566 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002567
dsansomee2d6fd92016-09-08 00:10:47 -07002568 def UpdateDescriptionRemote(self, description, force=False):
2569 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2570 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002571 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002572 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002573 'unpublished edit. Either publish the edit in the Gerrit web UI '
2574 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002575
2576 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2577 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002578 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002579 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002580
Aaron Gable636b13f2017-07-14 10:42:48 -07002581 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002582 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002583 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002584
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002585 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002586 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002587 messages = self._GetChangeDetail(
2588 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2589 file_comments = gerrit_util.GetChangeComments(
2590 self._GetGerritHost(), self.GetIssue())
2591
2592 # Build dictionary of file comments for easy access and sorting later.
2593 # {author+date: {path: {patchset: {line: url+message}}}}
2594 comments = collections.defaultdict(
2595 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2596 for path, line_comments in file_comments.iteritems():
2597 for comment in line_comments:
2598 if comment.get('tag', '').startswith('autogenerated'):
2599 continue
2600 key = (comment['author']['email'], comment['updated'])
2601 if comment.get('side', 'REVISION') == 'PARENT':
2602 patchset = 'Base'
2603 else:
2604 patchset = 'PS%d' % comment['patch_set']
2605 line = comment.get('line', 0)
2606 url = ('https://%s/c/%s/%s/%s#%s%s' %
2607 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2608 'b' if comment.get('side') == 'PARENT' else '',
2609 str(line) if line else ''))
2610 comments[key][path][patchset][line] = (url, comment['message'])
2611
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002612 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002613 for msg in messages:
2614 # Don't bother showing autogenerated messages.
2615 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2616 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002617 # Gerrit spits out nanoseconds.
2618 assert len(msg['date'].split('.')[-1]) == 9
2619 date = datetime.datetime.strptime(msg['date'][:-3],
2620 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002621 message = msg['message']
2622 key = (msg['author']['email'], msg['date'])
2623 if key in comments:
2624 message += '\n'
2625 for path, patchsets in sorted(comments.get(key, {}).items()):
2626 if readable:
2627 message += '\n%s' % path
2628 for patchset, lines in sorted(patchsets.items()):
2629 for line, (url, content) in sorted(lines.items()):
2630 if line:
2631 line_str = 'Line %d' % line
2632 path_str = '%s:%d:' % (path, line)
2633 else:
2634 line_str = 'File comment'
2635 path_str = '%s:0:' % path
2636 if readable:
2637 message += '\n %s, %s: %s' % (patchset, line_str, url)
2638 message += '\n %s\n' % content
2639 else:
2640 message += '\n%s ' % path_str
2641 message += '\n%s\n' % content
2642
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002643 summary.append(_CommentSummary(
2644 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002645 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002646 sender=msg['author']['email'],
2647 # These could be inferred from the text messages and correlated with
2648 # Code-Review label maximum, however this is not reliable.
2649 # Leaving as is until the need arises.
2650 approval=False,
2651 disapproval=False,
2652 ))
2653 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002654
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002655 def CloseIssue(self):
2656 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2657
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002658 def SubmitIssue(self, wait_for_merge=True):
2659 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2660 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002661
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002662 def _GetChangeDetail(self, options=None, issue=None,
2663 no_cache=False):
2664 """Returns details of the issue by querying Gerrit and caching results.
2665
2666 If fresh data is needed, set no_cache=True which will clear cache and
2667 thus new data will be fetched from Gerrit.
2668 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002669 options = options or []
2670 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002671 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002672
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002673 # Optimization to avoid multiple RPCs:
2674 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2675 'CURRENT_COMMIT' not in options):
2676 options.append('CURRENT_COMMIT')
2677
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002678 # Normalize issue and options for consistent keys in cache.
2679 issue = str(issue)
2680 options = [o.upper() for o in options]
2681
2682 # Check in cache first unless no_cache is True.
2683 if no_cache:
2684 self._detail_cache.pop(issue, None)
2685 else:
2686 options_set = frozenset(options)
2687 for cached_options_set, data in self._detail_cache.get(issue, []):
2688 # Assumption: data fetched before with extra options is suitable
2689 # for return for a smaller set of options.
2690 # For example, if we cached data for
2691 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2692 # and request is for options=[CURRENT_REVISION],
2693 # THEN we can return prior cached data.
2694 if options_set.issubset(cached_options_set):
2695 return data
2696
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002697 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002698 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002699 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002700 except gerrit_util.GerritError as e:
2701 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002702 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002703 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002704
2705 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002706 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002707
agable32978d92016-11-01 12:55:02 -07002708 def _GetChangeCommit(self, issue=None):
2709 issue = issue or self.GetIssue()
2710 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002711 try:
2712 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2713 except gerrit_util.GerritError as e:
2714 if e.http_status == 404:
2715 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2716 raise
agable32978d92016-11-01 12:55:02 -07002717 return data
2718
Olivier Robin75ee7252018-04-13 10:02:56 +02002719 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002720 if git_common.is_dirty_git_tree('land'):
2721 return 1
tandriid60367b2016-06-22 05:25:12 -07002722 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2723 if u'Commit-Queue' in detail.get('labels', {}):
2724 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002725 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2726 'which can test and land changes for you. '
2727 'Are you sure you wish to bypass it?\n',
2728 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002729
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002730 differs = True
tandriic4344b52016-08-29 06:04:54 -07002731 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002732 # Note: git diff outputs nothing if there is no diff.
2733 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002734 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002735 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002736 if detail['current_revision'] == last_upload:
2737 differs = False
2738 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002739 print('WARNING: Local branch contents differ from latest uploaded '
2740 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002741 if differs:
2742 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002743 confirm_or_exit(
2744 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2745 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002746 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002747 elif not bypass_hooks:
2748 hook_results = self.RunHook(
2749 committing=True,
2750 may_prompt=not force,
2751 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002752 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2753 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002754 if not hook_results.should_continue():
2755 return 1
2756
2757 self.SubmitIssue(wait_for_merge=True)
2758 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002759 links = self._GetChangeCommit().get('web_links', [])
2760 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002761 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002762 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002763 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002764 return 0
2765
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002766 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002767 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002768 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002769 assert not directory
2770 assert parsed_issue_arg.valid
2771
2772 self._changelist.issue = parsed_issue_arg.issue
2773
2774 if parsed_issue_arg.hostname:
2775 self._gerrit_host = parsed_issue_arg.hostname
2776 self._gerrit_server = 'https://%s' % self._gerrit_host
2777
tandriic2405f52016-10-10 08:13:15 -07002778 try:
2779 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002780 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002781 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002782
2783 if not parsed_issue_arg.patchset:
2784 # Use current revision by default.
2785 revision_info = detail['revisions'][detail['current_revision']]
2786 patchset = int(revision_info['_number'])
2787 else:
2788 patchset = parsed_issue_arg.patchset
2789 for revision_info in detail['revisions'].itervalues():
2790 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2791 break
2792 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002793 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002794 (parsed_issue_arg.patchset, self.GetIssue()))
2795
Aaron Gable697a91b2018-01-19 15:20:15 -08002796 remote_url = self._changelist.GetRemoteUrl()
2797 if remote_url.endswith('.git'):
2798 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002799 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002800
2801 if remote_url != fetch_info['url']:
2802 DieWithError('Trying to patch a change from %s but this repo appears '
2803 'to be %s.' % (fetch_info['url'], remote_url))
2804
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002805 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002806
Aaron Gable62619a32017-06-16 08:22:09 -07002807 if force:
2808 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2809 print('Checked out commit for change %i patchset %i locally' %
2810 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002811 elif nocommit:
2812 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2813 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002814 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002815 RunGit(['cherry-pick', 'FETCH_HEAD'])
2816 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002817 (parsed_issue_arg.issue, patchset))
2818 print('Note: this created a local commit which does not have '
2819 'the same hash as the one uploaded for review. This will make '
2820 'uploading changes based on top of this branch difficult.\n'
2821 'If you want to do that, use "git cl patch --force" instead.')
2822
Stefan Zagerd08043c2017-10-12 12:07:02 -07002823 if self.GetBranch():
2824 self.SetIssue(parsed_issue_arg.issue)
2825 self.SetPatchset(patchset)
2826 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2827 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2828 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2829 else:
2830 print('WARNING: You are in detached HEAD state.\n'
2831 'The patch has been applied to your checkout, but you will not be '
2832 'able to upload a new patch set to the gerrit issue.\n'
2833 'Try using the \'-b\' option if you would like to work on a '
2834 'branch and/or upload a new patch set.')
2835
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002836 return 0
2837
2838 @staticmethod
2839 def ParseIssueURL(parsed_url):
2840 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2841 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002842 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2843 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002844 # Short urls like https://domain/<issue_number> can be used, but don't allow
2845 # specifying the patchset (you'd 404), but we allow that here.
2846 if parsed_url.path == '/':
2847 part = parsed_url.fragment
2848 else:
2849 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002850 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002851 if match:
2852 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002853 issue=int(match.group(3)),
2854 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002855 hostname=parsed_url.netloc,
2856 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002857 return None
2858
tandrii16e0b4e2016-06-07 10:34:28 -07002859 def _GerritCommitMsgHookCheck(self, offer_removal):
2860 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2861 if not os.path.exists(hook):
2862 return
2863 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2864 # custom developer made one.
2865 data = gclient_utils.FileRead(hook)
2866 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2867 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002868 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002869 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002870 'and may interfere with it in subtle ways.\n'
2871 'We recommend you remove the commit-msg hook.')
2872 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002873 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002874 gclient_utils.rm_file_or_tree(hook)
2875 print('Gerrit commit-msg hook removed.')
2876 else:
2877 print('OK, will keep Gerrit commit-msg hook in place.')
2878
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002879 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002880 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002881 if options.squash and options.no_squash:
2882 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002883
2884 if not options.squash and not options.no_squash:
2885 # Load default for user, repo, squash=true, in this order.
2886 options.squash = settings.GetSquashGerritUploads()
2887 elif options.no_squash:
2888 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002889
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002890 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002891 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002892
Aaron Gableb56ad332017-01-06 15:24:31 -08002893 # This may be None; default fallback value is determined in logic below.
2894 title = options.title
2895
Dominic Battre7d1c4842017-10-27 09:17:28 +02002896 # Extract bug number from branch name.
2897 bug = options.bug
2898 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2899 if not bug and match:
2900 bug = match.group(1)
2901
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002902 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002903 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 if self.GetIssue():
2905 # Try to get the message from a previous upload.
2906 message = self.GetDescription()
2907 if not message:
2908 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002909 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002911 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002912 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002913 # When uploading a subsequent patchset, -m|--message is taken
2914 # as the patchset title if --title was not provided.
2915 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002916 else:
2917 default_title = RunGit(
2918 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002919 if options.force:
2920 title = default_title
2921 else:
2922 title = ask_for_data(
2923 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002924 change_id = self._GetChangeDetail()['change_id']
2925 while True:
2926 footer_change_ids = git_footers.get_footer_change_id(message)
2927 if footer_change_ids == [change_id]:
2928 break
2929 if not footer_change_ids:
2930 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002931 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002932 continue
2933 # There is already a valid footer but with different or several ids.
2934 # Doing this automatically is non-trivial as we don't want to lose
2935 # existing other footers, yet we want to append just 1 desired
2936 # Change-Id. Thus, just create a new footer, but let user verify the
2937 # new description.
2938 message = '%s\n\nChange-Id: %s' % (message, change_id)
2939 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002940 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002941 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002942 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002943 'Please, check the proposed correction to the description, '
2944 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2945 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2946 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002947 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002948 if not options.force:
2949 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002950 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002951 message = change_desc.description
2952 if not message:
2953 DieWithError("Description is empty. Aborting...")
2954 # Continue the while loop.
2955 # Sanity check of this code - we should end up with proper message
2956 # footer.
2957 assert [change_id] == git_footers.get_footer_change_id(message)
2958 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002959 else: # if not self.GetIssue()
2960 if options.message:
2961 message = options.message
2962 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002963 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002964 if options.title:
2965 message = options.title + '\n\n' + message
2966 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002967
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002968 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002969 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002970 # On first upload, patchset title is always this string, while
2971 # --title flag gets converted to first line of message.
2972 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002973 if not change_desc.description:
2974 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002975 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002976 if len(change_ids) > 1:
2977 DieWithError('too many Change-Id footers, at most 1 allowed.')
2978 if not change_ids:
2979 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002980 change_desc.set_description(git_footers.add_footer_change_id(
2981 change_desc.description,
2982 GenerateGerritChangeId(change_desc.description)))
2983 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002984 assert len(change_ids) == 1
2985 change_id = change_ids[0]
2986
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002987 if options.reviewers or options.tbrs or options.add_owners_to:
2988 change_desc.update_reviewers(options.reviewers, options.tbrs,
2989 options.add_owners_to, change)
2990
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002991 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002992 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2993 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002994 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002995 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2996 desc_tempfile.write(change_desc.description)
2997 desc_tempfile.close()
2998 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2999 '-F', desc_tempfile.name]).strip()
3000 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003001 else:
3002 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003003 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003004 if not change_desc.description:
3005 DieWithError("Description is empty. Aborting...")
3006
3007 if not git_footers.get_footer_change_id(change_desc.description):
3008 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003009 change_desc.set_description(
3010 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003011 if options.reviewers or options.tbrs or options.add_owners_to:
3012 change_desc.update_reviewers(options.reviewers, options.tbrs,
3013 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003014 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003015 # For no-squash mode, we assume the remote called "origin" is the one we
3016 # want. It is not worthwhile to support different workflows for
3017 # no-squash mode.
3018 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003019 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3020
3021 assert change_desc
3022 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3023 ref_to_push)]).splitlines()
3024 if len(commits) > 1:
3025 print('WARNING: This will upload %d commits. Run the following command '
3026 'to see which commits will be uploaded: ' % len(commits))
3027 print('git log %s..%s' % (parent, ref_to_push))
3028 print('You can also use `git squash-branch` to squash these into a '
3029 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003030 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003031
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003032 if options.reviewers or options.tbrs or options.add_owners_to:
3033 change_desc.update_reviewers(options.reviewers, options.tbrs,
3034 options.add_owners_to, change)
3035
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003036 # Extra options that can be specified at push time. Doc:
3037 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003038 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003039
Aaron Gable844cf292017-06-28 11:32:59 -07003040 # By default, new changes are started in WIP mode, and subsequent patchsets
3041 # don't send email. At any time, passing --send-mail will mark the change
3042 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003043 if options.send_mail:
3044 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003045 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003046 elif not self.GetIssue():
3047 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003048 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003049 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003050
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003051 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003052 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003053
Aaron Gable9b713dd2016-12-14 16:04:21 -08003054 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003055 # Punctuation and whitespace in |title| must be percent-encoded.
3056 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003057
agablec6787972016-09-09 16:13:34 -07003058 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003059 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003060
rmistry9eadede2016-09-19 11:22:43 -07003061 if options.topic:
3062 # Documentation on Gerrit topics is here:
3063 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003064 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003065
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003066 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003067 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003068 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003069 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003070 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3071
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003072 refspec_suffix = ''
3073 if refspec_opts:
3074 refspec_suffix = '%' + ','.join(refspec_opts)
3075 assert ' ' not in refspec_suffix, (
3076 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3077 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3078
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003079 try:
3080 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003081 ['git', 'push', self.GetRemoteUrl(), refspec],
3082 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003083 # Flush after every line: useful for seeing progress when running as
3084 # recipe.
3085 filter_fn=lambda _: sys.stdout.flush())
3086 except subprocess2.CalledProcessError:
3087 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003088 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003089 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003090 'credential problems:\n'
3091 ' git cl creds-check\n',
3092 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003093
3094 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003095 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003096 change_numbers = [m.group(1)
3097 for m in map(regex.match, push_stdout.splitlines())
3098 if m]
3099 if len(change_numbers) != 1:
3100 DieWithError(
3101 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003102 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003103 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003104 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003105
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003106 reviewers = sorted(change_desc.get_reviewers())
3107
tandrii88189772016-09-29 04:29:57 -07003108 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003109 if not options.private:
3110 cc = self.GetCCList().split(',')
3111 else:
3112 cc = []
tandrii88189772016-09-29 04:29:57 -07003113 if options.cc:
3114 cc.extend(options.cc)
3115 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003116 if change_desc.get_cced():
3117 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003118
3119 gerrit_util.AddReviewers(
3120 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3121 notify=bool(options.send_mail))
3122
Aaron Gablefd238082017-06-07 13:42:34 -07003123 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003124 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3125 score = 1
3126 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3127 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3128 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003129 gerrit_util.SetReview(
3130 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003131 msg='Self-approving for TBR',
3132 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003133
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003134 return 0
3135
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003136 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3137 change_desc):
3138 """Computes parent of the generated commit to be uploaded to Gerrit.
3139
3140 Returns revision or a ref name.
3141 """
3142 if custom_cl_base:
3143 # Try to avoid creating additional unintended CLs when uploading, unless
3144 # user wants to take this risk.
3145 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3146 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3147 local_ref_of_target_remote])
3148 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003149 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003150 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3151 'If you proceed with upload, more than 1 CL may be created by '
3152 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3153 'If you are certain that specified base `%s` has already been '
3154 'uploaded to Gerrit as another CL, you may proceed.\n' %
3155 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3156 if not force:
3157 confirm_or_exit(
3158 'Do you take responsibility for cleaning up potential mess '
3159 'resulting from proceeding with upload?',
3160 action='upload')
3161 return custom_cl_base
3162
Aaron Gablef97e33d2017-03-30 15:44:27 -07003163 if remote != '.':
3164 return self.GetCommonAncestorWithUpstream()
3165
3166 # If our upstream branch is local, we base our squashed commit on its
3167 # squashed version.
3168 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3169
Aaron Gablef97e33d2017-03-30 15:44:27 -07003170 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003171 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003172
3173 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003174 # TODO(tandrii): consider checking parent change in Gerrit and using its
3175 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3176 # the tree hash of the parent branch. The upside is less likely bogus
3177 # requests to reupload parent change just because it's uploadhash is
3178 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003179 parent = RunGit(['config',
3180 'branch.%s.gerritsquashhash' % upstream_branch_name],
3181 error_ok=True).strip()
3182 # Verify that the upstream branch has been uploaded too, otherwise
3183 # Gerrit will create additional CLs when uploading.
3184 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3185 RunGitSilent(['rev-parse', parent + ':'])):
3186 DieWithError(
3187 '\nUpload upstream branch %s first.\n'
3188 'It is likely that this branch has been rebased since its last '
3189 'upload, so you just need to upload it again.\n'
3190 '(If you uploaded it with --no-squash, then branch dependencies '
3191 'are not supported, and you should reupload with --squash.)'
3192 % upstream_branch_name,
3193 change_desc)
3194 return parent
3195
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003196 def _AddChangeIdToCommitMessage(self, options, args):
3197 """Re-commits using the current message, assumes the commit hook is in
3198 place.
3199 """
3200 log_desc = options.message or CreateDescriptionFromLog(args)
3201 git_command = ['commit', '--amend', '-m', log_desc]
3202 RunGit(git_command)
3203 new_log_desc = CreateDescriptionFromLog(args)
3204 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003205 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003206 return new_log_desc
3207 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003208 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003209
Ravi Mistry31e7d562018-04-02 12:53:57 -04003210 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3211 """Sets labels on the change based on the provided flags."""
3212 labels = {}
3213 notify = None;
3214 if enable_auto_submit:
3215 labels['Auto-Submit'] = 1
3216 if use_commit_queue:
3217 labels['Commit-Queue'] = 2
3218 elif cq_dry_run:
3219 labels['Commit-Queue'] = 1
3220 notify = False
3221 if labels:
3222 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3223 labels=labels, notify=notify)
3224
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003225 def SetCQState(self, new_state):
3226 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003227 vote_map = {
3228 _CQState.NONE: 0,
3229 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003230 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003231 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003232 labels = {'Commit-Queue': vote_map[new_state]}
3233 notify = False if new_state == _CQState.DRY_RUN else None
3234 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3235 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003236
tandriie113dfd2016-10-11 10:20:12 -07003237 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003238 try:
3239 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003240 except GerritChangeNotExists:
3241 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003242
3243 if data['status'] in ('ABANDONED', 'MERGED'):
3244 return 'CL %s is closed' % self.GetIssue()
3245
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003246 def GetTryJobProperties(self, patchset=None):
3247 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003248 data = self._GetChangeDetail(['ALL_REVISIONS'])
3249 patchset = int(patchset or self.GetPatchset())
3250 assert patchset
3251 revision_data = None # Pylint wants it to be defined.
3252 for revision_data in data['revisions'].itervalues():
3253 if int(revision_data['_number']) == patchset:
3254 break
3255 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003256 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003257 (patchset, self.GetIssue()))
3258 return {
3259 'patch_issue': self.GetIssue(),
3260 'patch_set': patchset or self.GetPatchset(),
3261 'patch_project': data['project'],
3262 'patch_storage': 'gerrit',
3263 'patch_ref': revision_data['fetch']['http']['ref'],
3264 'patch_repository_url': revision_data['fetch']['http']['url'],
3265 'patch_gerrit_url': self.GetCodereviewServer(),
3266 }
tandriie113dfd2016-10-11 10:20:12 -07003267
tandriide281ae2016-10-12 06:02:30 -07003268 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003269 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003270
Edward Lemur707d70b2018-02-07 00:50:14 +01003271 def GetReviewers(self):
3272 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3273 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3274
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003275
3276_CODEREVIEW_IMPLEMENTATIONS = {
3277 'rietveld': _RietveldChangelistImpl,
3278 'gerrit': _GerritChangelistImpl,
3279}
3280
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003281
iannuccie53c9352016-08-17 14:40:40 -07003282def _add_codereview_issue_select_options(parser, extra=""):
3283 _add_codereview_select_options(parser)
3284
3285 text = ('Operate on this issue number instead of the current branch\'s '
3286 'implicit issue.')
3287 if extra:
3288 text += ' '+extra
3289 parser.add_option('-i', '--issue', type=int, help=text)
3290
3291
3292def _process_codereview_issue_select_options(parser, options):
3293 _process_codereview_select_options(parser, options)
3294 if options.issue is not None and not options.forced_codereview:
3295 parser.error('--issue must be specified with either --rietveld or --gerrit')
3296
3297
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003298def _add_codereview_select_options(parser):
3299 """Appends --gerrit and --rietveld options to force specific codereview."""
3300 parser.codereview_group = optparse.OptionGroup(
3301 parser, 'EXPERIMENTAL! Codereview override options')
3302 parser.add_option_group(parser.codereview_group)
3303 parser.codereview_group.add_option(
3304 '--gerrit', action='store_true',
3305 help='Force the use of Gerrit for codereview')
3306 parser.codereview_group.add_option(
3307 '--rietveld', action='store_true',
3308 help='Force the use of Rietveld for codereview')
3309
3310
3311def _process_codereview_select_options(parser, options):
3312 if options.gerrit and options.rietveld:
3313 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3314 options.forced_codereview = None
3315 if options.gerrit:
3316 options.forced_codereview = 'gerrit'
3317 elif options.rietveld:
3318 options.forced_codereview = 'rietveld'
3319
3320
tandriif9aefb72016-07-01 09:06:51 -07003321def _get_bug_line_values(default_project, bugs):
3322 """Given default_project and comma separated list of bugs, yields bug line
3323 values.
3324
3325 Each bug can be either:
3326 * a number, which is combined with default_project
3327 * string, which is left as is.
3328
3329 This function may produce more than one line, because bugdroid expects one
3330 project per line.
3331
3332 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3333 ['v8:123', 'chromium:789']
3334 """
3335 default_bugs = []
3336 others = []
3337 for bug in bugs.split(','):
3338 bug = bug.strip()
3339 if bug:
3340 try:
3341 default_bugs.append(int(bug))
3342 except ValueError:
3343 others.append(bug)
3344
3345 if default_bugs:
3346 default_bugs = ','.join(map(str, default_bugs))
3347 if default_project:
3348 yield '%s:%s' % (default_project, default_bugs)
3349 else:
3350 yield default_bugs
3351 for other in sorted(others):
3352 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3353 yield other
3354
3355
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003356class ChangeDescription(object):
3357 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003358 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003359 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003360 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003361 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003362 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3363 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3364 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3365 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003366
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003367 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003368 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003369
agable@chromium.org42c20792013-09-12 17:34:49 +00003370 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003371 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003372 return '\n'.join(self._description_lines)
3373
3374 def set_description(self, desc):
3375 if isinstance(desc, basestring):
3376 lines = desc.splitlines()
3377 else:
3378 lines = [line.rstrip() for line in desc]
3379 while lines and not lines[0]:
3380 lines.pop(0)
3381 while lines and not lines[-1]:
3382 lines.pop(-1)
3383 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003384
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003385 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3386 """Rewrites the R=/TBR= line(s) as a single line each.
3387
3388 Args:
3389 reviewers (list(str)) - list of additional emails to use for reviewers.
3390 tbrs (list(str)) - list of additional emails to use for TBRs.
3391 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3392 the change that are missing OWNER coverage. If this is not None, you
3393 must also pass a value for `change`.
3394 change (Change) - The Change that should be used for OWNERS lookups.
3395 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003396 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003397 assert isinstance(tbrs, list), tbrs
3398
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003399 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003400 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003401
3402 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003403 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003404
3405 reviewers = set(reviewers)
3406 tbrs = set(tbrs)
3407 LOOKUP = {
3408 'TBR': tbrs,
3409 'R': reviewers,
3410 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003411
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003412 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003413 regexp = re.compile(self.R_LINE)
3414 matches = [regexp.match(line) for line in self._description_lines]
3415 new_desc = [l for i, l in enumerate(self._description_lines)
3416 if not matches[i]]
3417 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003418
agable@chromium.org42c20792013-09-12 17:34:49 +00003419 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003420
3421 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003422 for match in matches:
3423 if not match:
3424 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003425 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3426
3427 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003428 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003429 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003430 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003431 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003432 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003433 LOOKUP[add_owners_to].update(
3434 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003435
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003436 # If any folks ended up in both groups, remove them from tbrs.
3437 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003438
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003439 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3440 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003441
3442 # Put the new lines in the description where the old first R= line was.
3443 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3444 if 0 <= line_loc < len(self._description_lines):
3445 if new_tbr_line:
3446 self._description_lines.insert(line_loc, new_tbr_line)
3447 if new_r_line:
3448 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003449 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003450 if new_r_line:
3451 self.append_footer(new_r_line)
3452 if new_tbr_line:
3453 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003454
Aaron Gable3a16ed12017-03-23 10:51:55 -07003455 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003456 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003457 self.set_description([
3458 '# Enter a description of the change.',
3459 '# This will be displayed on the codereview site.',
3460 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003461 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003462 '--------------------',
3463 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003464
agable@chromium.org42c20792013-09-12 17:34:49 +00003465 regexp = re.compile(self.BUG_LINE)
3466 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003467 prefix = settings.GetBugPrefix()
3468 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003469 if git_footer:
3470 self.append_footer('Bug: %s' % ', '.join(values))
3471 else:
3472 for value in values:
3473 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003474
agable@chromium.org42c20792013-09-12 17:34:49 +00003475 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003476 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003477 if not content:
3478 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003479 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003480
Bruce Dawson2377b012018-01-11 16:46:49 -08003481 # Strip off comments and default inserted "Bug:" line.
3482 clean_lines = [line.rstrip() for line in lines if not
3483 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003484 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003485 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003486 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003487
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003488 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003489 """Adds a footer line to the description.
3490
3491 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3492 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3493 that Gerrit footers are always at the end.
3494 """
3495 parsed_footer_line = git_footers.parse_footer(line)
3496 if parsed_footer_line:
3497 # Line is a gerrit footer in the form: Footer-Key: any value.
3498 # Thus, must be appended observing Gerrit footer rules.
3499 self.set_description(
3500 git_footers.add_footer(self.description,
3501 key=parsed_footer_line[0],
3502 value=parsed_footer_line[1]))
3503 return
3504
3505 if not self._description_lines:
3506 self._description_lines.append(line)
3507 return
3508
3509 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3510 if gerrit_footers:
3511 # git_footers.split_footers ensures that there is an empty line before
3512 # actual (gerrit) footers, if any. We have to keep it that way.
3513 assert top_lines and top_lines[-1] == ''
3514 top_lines, separator = top_lines[:-1], top_lines[-1:]
3515 else:
3516 separator = [] # No need for separator if there are no gerrit_footers.
3517
3518 prev_line = top_lines[-1] if top_lines else ''
3519 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3520 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3521 top_lines.append('')
3522 top_lines.append(line)
3523 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003524
tandrii99a72f22016-08-17 14:33:24 -07003525 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003526 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003527 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003528 reviewers = [match.group(2).strip()
3529 for match in matches
3530 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003531 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003532
bradnelsond975b302016-10-23 12:20:23 -07003533 def get_cced(self):
3534 """Retrieves the list of reviewers."""
3535 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3536 cced = [match.group(2).strip() for match in matches if match]
3537 return cleanup_list(cced)
3538
Nodir Turakulov23b82142017-11-16 11:04:25 -08003539 def get_hash_tags(self):
3540 """Extracts and sanitizes a list of Gerrit hashtags."""
3541 subject = (self._description_lines or ('',))[0]
3542 subject = re.sub(
3543 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3544
3545 tags = []
3546 start = 0
3547 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3548 while True:
3549 m = bracket_exp.match(subject, start)
3550 if not m:
3551 break
3552 tags.append(self.sanitize_hash_tag(m.group(1)))
3553 start = m.end()
3554
3555 if not tags:
3556 # Try "Tag: " prefix.
3557 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3558 if m:
3559 tags.append(self.sanitize_hash_tag(m.group(1)))
3560 return tags
3561
3562 @classmethod
3563 def sanitize_hash_tag(cls, tag):
3564 """Returns a sanitized Gerrit hash tag.
3565
3566 A sanitized hashtag can be used as a git push refspec parameter value.
3567 """
3568 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3569
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003570 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3571 """Updates this commit description given the parent.
3572
3573 This is essentially what Gnumbd used to do.
3574 Consult https://goo.gl/WMmpDe for more details.
3575 """
3576 assert parent_msg # No, orphan branch creation isn't supported.
3577 assert parent_hash
3578 assert dest_ref
3579 parent_footer_map = git_footers.parse_footers(parent_msg)
3580 # This will also happily parse svn-position, which GnumbD is no longer
3581 # supporting. While we'd generate correct footers, the verifier plugin
3582 # installed in Gerrit will block such commit (ie git push below will fail).
3583 parent_position = git_footers.get_position(parent_footer_map)
3584
3585 # Cherry-picks may have last line obscuring their prior footers,
3586 # from git_footers perspective. This is also what Gnumbd did.
3587 cp_line = None
3588 if (self._description_lines and
3589 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3590 cp_line = self._description_lines.pop()
3591
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003592 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003593
3594 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3595 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003596 for i, line in enumerate(footer_lines):
3597 k, v = git_footers.parse_footer(line) or (None, None)
3598 if k and k.startswith('Cr-'):
3599 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003600
3601 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003602 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003603 if parent_position[0] == dest_ref:
3604 # Same branch as parent.
3605 number = int(parent_position[1]) + 1
3606 else:
3607 number = 1 # New branch, and extra lineage.
3608 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3609 int(parent_position[1])))
3610
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003611 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3612 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003613
3614 self._description_lines = top_lines
3615 if cp_line:
3616 self._description_lines.append(cp_line)
3617 if self._description_lines[-1] != '':
3618 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003619 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003620
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003621
Aaron Gablea1bab272017-04-11 16:38:18 -07003622def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003623 """Retrieves the reviewers that approved a CL from the issue properties with
3624 messages.
3625
3626 Note that the list may contain reviewers that are not committer, thus are not
3627 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003628
3629 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003630 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003631 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003632 return sorted(
3633 set(
3634 message['sender']
3635 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003636 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003637 )
3638 )
3639
3640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641def FindCodereviewSettingsFile(filename='codereview.settings'):
3642 """Finds the given file starting in the cwd and going up.
3643
3644 Only looks up to the top of the repository unless an
3645 'inherit-review-settings-ok' file exists in the root of the repository.
3646 """
3647 inherit_ok_file = 'inherit-review-settings-ok'
3648 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003649 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003650 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3651 root = '/'
3652 while True:
3653 if filename in os.listdir(cwd):
3654 if os.path.isfile(os.path.join(cwd, filename)):
3655 return open(os.path.join(cwd, filename))
3656 if cwd == root:
3657 break
3658 cwd = os.path.dirname(cwd)
3659
3660
3661def LoadCodereviewSettingsFromFile(fileobj):
3662 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003663 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 def SetProperty(name, setting, unset_error_ok=False):
3666 fullname = 'rietveld.' + name
3667 if setting in keyvals:
3668 RunGit(['config', fullname, keyvals[setting]])
3669 else:
3670 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3671
tandrii48df5812016-10-17 03:55:37 -07003672 if not keyvals.get('GERRIT_HOST', False):
3673 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003674 # Only server setting is required. Other settings can be absent.
3675 # In that case, we ignore errors raised during option deletion attempt.
3676 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003677 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003678 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3679 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003680 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003681 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3682 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003683 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003684 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3685 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003686
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003687 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003688 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003689
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003690 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003691 RunGit(['config', 'gerrit.squash-uploads',
3692 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003693
tandrii@chromium.org28253532016-04-14 13:46:56 +00003694 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003695 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003696 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003698 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003699 # should be of the form
3700 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3701 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3703 keyvals['ORIGIN_URL_CONFIG']])
3704
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003705
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003706def urlretrieve(source, destination):
3707 """urllib is broken for SSL connections via a proxy therefore we
3708 can't use urllib.urlretrieve()."""
3709 with open(destination, 'w') as f:
3710 f.write(urllib2.urlopen(source).read())
3711
3712
ukai@chromium.org712d6102013-11-27 00:52:58 +00003713def hasSheBang(fname):
3714 """Checks fname is a #! script."""
3715 with open(fname) as f:
3716 return f.read(2).startswith('#!')
3717
3718
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003719# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3720def DownloadHooks(*args, **kwargs):
3721 pass
3722
3723
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003724def DownloadGerritHook(force):
3725 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003726
3727 Args:
3728 force: True to update hooks. False to install hooks if not present.
3729 """
3730 if not settings.GetIsGerrit():
3731 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003732 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003733 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3734 if not os.access(dst, os.X_OK):
3735 if os.path.exists(dst):
3736 if not force:
3737 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003738 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003739 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003740 if not hasSheBang(dst):
3741 DieWithError('Not a script: %s\n'
3742 'You need to download from\n%s\n'
3743 'into .git/hooks/commit-msg and '
3744 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003745 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3746 except Exception:
3747 if os.path.exists(dst):
3748 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003749 DieWithError('\nFailed to download hooks.\n'
3750 'You need to download from\n%s\n'
3751 'into .git/hooks/commit-msg and '
3752 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003753
3754
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003755def GetRietveldCodereviewSettingsInteractively():
3756 """Prompt the user for settings."""
3757 server = settings.GetDefaultServerUrl(error_ok=True)
3758 prompt = 'Rietveld server (host[:port])'
3759 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3760 newserver = ask_for_data(prompt + ':')
3761 if not server and not newserver:
3762 newserver = DEFAULT_SERVER
3763 if newserver:
3764 newserver = gclient_utils.UpgradeToHttps(newserver)
3765 if newserver != server:
3766 RunGit(['config', 'rietveld.server', newserver])
3767
3768 def SetProperty(initial, caption, name, is_url):
3769 prompt = caption
3770 if initial:
3771 prompt += ' ("x" to clear) [%s]' % initial
3772 new_val = ask_for_data(prompt + ':')
3773 if new_val == 'x':
3774 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3775 elif new_val:
3776 if is_url:
3777 new_val = gclient_utils.UpgradeToHttps(new_val)
3778 if new_val != initial:
3779 RunGit(['config', 'rietveld.' + name, new_val])
3780
3781 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3782 SetProperty(settings.GetDefaultPrivateFlag(),
3783 'Private flag (rietveld only)', 'private', False)
3784 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3785 'tree-status-url', False)
3786 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3787 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3788 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3789 'run-post-upload-hook', False)
3790
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003791
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003792class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003793 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003794
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003795 _GOOGLESOURCE = 'googlesource.com'
3796
3797 def __init__(self):
3798 # Cached list of [host, identity, source], where source is either
3799 # .gitcookies or .netrc.
3800 self._all_hosts = None
3801
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003802 def ensure_configured_gitcookies(self):
3803 """Runs checks and suggests fixes to make git use .gitcookies from default
3804 path."""
3805 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3806 configured_path = RunGitSilent(
3807 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003808 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003809 if configured_path:
3810 self._ensure_default_gitcookies_path(configured_path, default)
3811 else:
3812 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003813
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003814 @staticmethod
3815 def _ensure_default_gitcookies_path(configured_path, default_path):
3816 assert configured_path
3817 if configured_path == default_path:
3818 print('git is already configured to use your .gitcookies from %s' %
3819 configured_path)
3820 return
3821
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003822 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003823 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3824 (configured_path, default_path))
3825
3826 if not os.path.exists(configured_path):
3827 print('However, your configured .gitcookies file is missing.')
3828 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3829 action='reconfigure')
3830 RunGit(['config', '--global', 'http.cookiefile', default_path])
3831 return
3832
3833 if os.path.exists(default_path):
3834 print('WARNING: default .gitcookies file already exists %s' %
3835 default_path)
3836 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3837 default_path)
3838
3839 confirm_or_exit('Move existing .gitcookies to default location?',
3840 action='move')
3841 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003842 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003843 print('Moved and reconfigured git to use .gitcookies from %s' %
3844 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003845
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003846 @staticmethod
3847 def _configure_gitcookies_path(default_path):
3848 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3849 if os.path.exists(netrc_path):
3850 print('You seem to be using outdated .netrc for git credentials: %s' %
3851 netrc_path)
3852 print('This tool will guide you through setting up recommended '
3853 '.gitcookies store for git credentials.\n'
3854 '\n'
3855 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3856 ' git config --global --unset http.cookiefile\n'
3857 ' mv %s %s.backup\n\n' % (default_path, default_path))
3858 confirm_or_exit(action='setup .gitcookies')
3859 RunGit(['config', '--global', 'http.cookiefile', default_path])
3860 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003861
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003862 def get_hosts_with_creds(self, include_netrc=False):
3863 if self._all_hosts is None:
3864 a = gerrit_util.CookiesAuthenticator()
3865 self._all_hosts = [
3866 (h, u, s)
3867 for h, u, s in itertools.chain(
3868 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3869 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3870 )
3871 if h.endswith(self._GOOGLESOURCE)
3872 ]
3873
3874 if include_netrc:
3875 return self._all_hosts
3876 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3877
3878 def print_current_creds(self, include_netrc=False):
3879 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3880 if not hosts:
3881 print('No Git/Gerrit credentials found')
3882 return
3883 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3884 header = [('Host', 'User', 'Which file'),
3885 ['=' * l for l in lengths]]
3886 for row in (header + hosts):
3887 print('\t'.join((('%%+%ds' % l) % s)
3888 for l, s in zip(lengths, row)))
3889
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003890 @staticmethod
3891 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003892 """Parses identity "git-<username>.domain" into <username> and domain."""
3893 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003894 # distinguishable from sub-domains. But we do know typical domains:
3895 if identity.endswith('.chromium.org'):
3896 domain = 'chromium.org'
3897 username = identity[:-len('.chromium.org')]
3898 else:
3899 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003900 if username.startswith('git-'):
3901 username = username[len('git-'):]
3902 return username, domain
3903
3904 def _get_usernames_of_domain(self, domain):
3905 """Returns list of usernames referenced by .gitcookies in a given domain."""
3906 identities_by_domain = {}
3907 for _, identity, _ in self.get_hosts_with_creds():
3908 username, domain = self._parse_identity(identity)
3909 identities_by_domain.setdefault(domain, []).append(username)
3910 return identities_by_domain.get(domain)
3911
3912 def _canonical_git_googlesource_host(self, host):
3913 """Normalizes Gerrit hosts (with '-review') to Git host."""
3914 assert host.endswith(self._GOOGLESOURCE)
3915 # Prefix doesn't include '.' at the end.
3916 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3917 if prefix.endswith('-review'):
3918 prefix = prefix[:-len('-review')]
3919 return prefix + '.' + self._GOOGLESOURCE
3920
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003921 def _canonical_gerrit_googlesource_host(self, host):
3922 git_host = self._canonical_git_googlesource_host(host)
3923 prefix = git_host.split('.', 1)[0]
3924 return prefix + '-review.' + self._GOOGLESOURCE
3925
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003926 def _get_counterpart_host(self, host):
3927 assert host.endswith(self._GOOGLESOURCE)
3928 git = self._canonical_git_googlesource_host(host)
3929 gerrit = self._canonical_gerrit_googlesource_host(git)
3930 return git if gerrit == host else gerrit
3931
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003932 def has_generic_host(self):
3933 """Returns whether generic .googlesource.com has been configured.
3934
3935 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3936 """
3937 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3938 if host == '.' + self._GOOGLESOURCE:
3939 return True
3940 return False
3941
3942 def _get_git_gerrit_identity_pairs(self):
3943 """Returns map from canonic host to pair of identities (Git, Gerrit).
3944
3945 One of identities might be None, meaning not configured.
3946 """
3947 host_to_identity_pairs = {}
3948 for host, identity, _ in self.get_hosts_with_creds():
3949 canonical = self._canonical_git_googlesource_host(host)
3950 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3951 idx = 0 if canonical == host else 1
3952 pair[idx] = identity
3953 return host_to_identity_pairs
3954
3955 def get_partially_configured_hosts(self):
3956 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003957 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3958 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3959 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003960
3961 def get_conflicting_hosts(self):
3962 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003963 host
3964 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003965 if None not in (i1, i2) and i1 != i2)
3966
3967 def get_duplicated_hosts(self):
3968 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3969 return set(host for host, count in counters.iteritems() if count > 1)
3970
3971 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3972 'chromium.googlesource.com': 'chromium.org',
3973 'chrome-internal.googlesource.com': 'google.com',
3974 }
3975
3976 def get_hosts_with_wrong_identities(self):
3977 """Finds hosts which **likely** reference wrong identities.
3978
3979 Note: skips hosts which have conflicting identities for Git and Gerrit.
3980 """
3981 hosts = set()
3982 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3983 pair = self._get_git_gerrit_identity_pairs().get(host)
3984 if pair and pair[0] == pair[1]:
3985 _, domain = self._parse_identity(pair[0])
3986 if domain != expected:
3987 hosts.add(host)
3988 return hosts
3989
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003990 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003991 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003992 hosts = sorted(hosts)
3993 assert hosts
3994 if extra_column_func is None:
3995 extras = [''] * len(hosts)
3996 else:
3997 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003998 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3999 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004000 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004001 lines.append(tmpl % he)
4002 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004003
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004004 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004005 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004006 yield ('.googlesource.com wildcard record detected',
4007 ['Chrome Infrastructure team recommends to list full host names '
4008 'explicitly.'],
4009 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004010
4011 dups = self.get_duplicated_hosts()
4012 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004013 yield ('The following hosts were defined twice',
4014 self._format_hosts(dups),
4015 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004016
4017 partial = self.get_partially_configured_hosts()
4018 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004019 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4020 'These hosts are missing',
4021 self._format_hosts(partial, lambda host: 'but %s defined' %
4022 self._get_counterpart_host(host)),
4023 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004024
4025 conflicting = self.get_conflicting_hosts()
4026 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004027 yield ('The following Git hosts have differing credentials from their '
4028 'Gerrit counterparts',
4029 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4030 tuple(self._get_git_gerrit_identity_pairs()[host])),
4031 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004032
4033 wrong = self.get_hosts_with_wrong_identities()
4034 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004035 yield ('These hosts likely use wrong identity',
4036 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4037 (self._get_git_gerrit_identity_pairs()[host][0],
4038 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4039 wrong)
4040
4041 def find_and_report_problems(self):
4042 """Returns True if there was at least one problem, else False."""
4043 found = False
4044 bad_hosts = set()
4045 for title, sublines, hosts in self._find_problems():
4046 if not found:
4047 found = True
4048 print('\n\n.gitcookies problem report:\n')
4049 bad_hosts.update(hosts or [])
4050 print(' %s%s' % (title , (':' if sublines else '')))
4051 if sublines:
4052 print()
4053 print(' %s' % '\n '.join(sublines))
4054 print()
4055
4056 if bad_hosts:
4057 assert found
4058 print(' You can manually remove corresponding lines in your %s file and '
4059 'visit the following URLs with correct account to generate '
4060 'correct credential lines:\n' %
4061 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4062 print(' %s' % '\n '.join(sorted(set(
4063 gerrit_util.CookiesAuthenticator().get_new_password_url(
4064 self._canonical_git_googlesource_host(host))
4065 for host in bad_hosts
4066 ))))
4067 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004068
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004069
4070def CMDcreds_check(parser, args):
4071 """Checks credentials and suggests changes."""
4072 _, _ = parser.parse_args(args)
4073
4074 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004075 DieWithError(
4076 'This command is not designed for GCE, are you on a bot?\n'
4077 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004078
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004079 checker = _GitCookiesChecker()
4080 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004081
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004082 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004083 checker.print_current_creds(include_netrc=True)
4084
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004085 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004086 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004087 return 0
4088 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004089
4090
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004091@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004093 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004095 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004096 # TODO(tandrii): remove this once we switch to Gerrit.
4097 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004098 parser.add_option('--activate-update', action='store_true',
4099 help='activate auto-updating [rietveld] section in '
4100 '.git/config')
4101 parser.add_option('--deactivate-update', action='store_true',
4102 help='deactivate auto-updating [rietveld] section in '
4103 '.git/config')
4104 options, args = parser.parse_args(args)
4105
4106 if options.deactivate_update:
4107 RunGit(['config', 'rietveld.autoupdate', 'false'])
4108 return
4109
4110 if options.activate_update:
4111 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4112 return
4113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004115 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116 return 0
4117
4118 url = args[0]
4119 if not url.endswith('codereview.settings'):
4120 url = os.path.join(url, 'codereview.settings')
4121
4122 # Load code review settings and download hooks (if available).
4123 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4124 return 0
4125
4126
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004127def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004128 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004129 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4130 branch = ShortBranchName(branchref)
4131 _, args = parser.parse_args(args)
4132 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004134 return RunGit(['config', 'branch.%s.base-url' % branch],
4135 error_ok=False).strip()
4136 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004137 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004138 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4139 error_ok=False).strip()
4140
4141
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004142def color_for_status(status):
4143 """Maps a Changelist status to color, for CMDstatus and other tools."""
4144 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004145 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004146 'waiting': Fore.BLUE,
4147 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004148 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004149 'lgtm': Fore.GREEN,
4150 'commit': Fore.MAGENTA,
4151 'closed': Fore.CYAN,
4152 'error': Fore.WHITE,
4153 }.get(status, Fore.WHITE)
4154
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004155
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004156def get_cl_statuses(changes, fine_grained, max_processes=None):
4157 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004158
4159 If fine_grained is true, this will fetch CL statuses from the server.
4160 Otherwise, simply indicate if there's a matching url for the given branches.
4161
4162 If max_processes is specified, it is used as the maximum number of processes
4163 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4164 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004165
4166 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004167 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004168 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004169 upload.verbosity = 0
4170
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004171 if not changes:
4172 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004173
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004174 if not fine_grained:
4175 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004176 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004177 for cl in changes:
4178 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004179 return
4180
4181 # First, sort out authentication issues.
4182 logging.debug('ensuring credentials exist')
4183 for cl in changes:
4184 cl.EnsureAuthenticated(force=False, refresh=True)
4185
4186 def fetch(cl):
4187 try:
4188 return (cl, cl.GetStatus())
4189 except:
4190 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004191 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004192 raise
4193
4194 threads_count = len(changes)
4195 if max_processes:
4196 threads_count = max(1, min(threads_count, max_processes))
4197 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4198
4199 pool = ThreadPool(threads_count)
4200 fetched_cls = set()
4201 try:
4202 it = pool.imap_unordered(fetch, changes).__iter__()
4203 while True:
4204 try:
4205 cl, status = it.next(timeout=5)
4206 except multiprocessing.TimeoutError:
4207 break
4208 fetched_cls.add(cl)
4209 yield cl, status
4210 finally:
4211 pool.close()
4212
4213 # Add any branches that failed to fetch.
4214 for cl in set(changes) - fetched_cls:
4215 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004216
rmistry@google.com2dd99862015-06-22 12:22:18 +00004217
4218def upload_branch_deps(cl, args):
4219 """Uploads CLs of local branches that are dependents of the current branch.
4220
4221 If the local branch dependency tree looks like:
4222 test1 -> test2.1 -> test3.1
4223 -> test3.2
4224 -> test2.2 -> test3.3
4225
4226 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4227 run on the dependent branches in this order:
4228 test2.1, test3.1, test3.2, test2.2, test3.3
4229
4230 Note: This function does not rebase your local dependent branches. Use it when
4231 you make a change to the parent branch that will not conflict with its
4232 dependent branches, and you would like their dependencies updated in
4233 Rietveld.
4234 """
4235 if git_common.is_dirty_git_tree('upload-branch-deps'):
4236 return 1
4237
4238 root_branch = cl.GetBranch()
4239 if root_branch is None:
4240 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4241 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004242 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004243 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4244 'patchset dependencies without an uploaded CL.')
4245
4246 branches = RunGit(['for-each-ref',
4247 '--format=%(refname:short) %(upstream:short)',
4248 'refs/heads'])
4249 if not branches:
4250 print('No local branches found.')
4251 return 0
4252
4253 # Create a dictionary of all local branches to the branches that are dependent
4254 # on it.
4255 tracked_to_dependents = collections.defaultdict(list)
4256 for b in branches.splitlines():
4257 tokens = b.split()
4258 if len(tokens) == 2:
4259 branch_name, tracked = tokens
4260 tracked_to_dependents[tracked].append(branch_name)
4261
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print()
4263 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004264 dependents = []
4265 def traverse_dependents_preorder(branch, padding=''):
4266 dependents_to_process = tracked_to_dependents.get(branch, [])
4267 padding += ' '
4268 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004269 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004270 dependents.append(dependent)
4271 traverse_dependents_preorder(dependent, padding)
4272 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004273 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004274
4275 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004276 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004277 return 0
4278
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004279 confirm_or_exit('This command will checkout all dependent branches and run '
4280 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004281
andybons@chromium.org962f9462016-02-03 20:00:42 +00004282 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004283 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004284 args.extend(['-t', 'Updated patchset dependency'])
4285
rmistry@google.com2dd99862015-06-22 12:22:18 +00004286 # Record all dependents that failed to upload.
4287 failures = {}
4288 # Go through all dependents, checkout the branch and upload.
4289 try:
4290 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004291 print()
4292 print('--------------------------------------')
4293 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004294 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004295 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004296 try:
4297 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004298 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004299 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004300 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004301 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004302 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004303 finally:
4304 # Swap back to the original root branch.
4305 RunGit(['checkout', '-q', root_branch])
4306
vapiera7fbd5a2016-06-16 09:17:49 -07004307 print()
4308 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309 for dependent_branch in dependents:
4310 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004311 print(' %s : %s' % (dependent_branch, upload_status))
4312 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004313
4314 return 0
4315
4316
kmarshall3bff56b2016-06-06 18:31:47 -07004317def CMDarchive(parser, args):
4318 """Archives and deletes branches associated with closed changelists."""
4319 parser.add_option(
4320 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004321 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004322 parser.add_option(
4323 '-f', '--force', action='store_true',
4324 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004325 parser.add_option(
4326 '-d', '--dry-run', action='store_true',
4327 help='Skip the branch tagging and removal steps.')
4328 parser.add_option(
4329 '-t', '--notags', action='store_true',
4330 help='Do not tag archived branches. '
4331 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004332
4333 auth.add_auth_options(parser)
4334 options, args = parser.parse_args(args)
4335 if args:
4336 parser.error('Unsupported args: %s' % ' '.join(args))
4337 auth_config = auth.extract_auth_config_from_options(options)
4338
4339 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4340 if not branches:
4341 return 0
4342
vapiera7fbd5a2016-06-16 09:17:49 -07004343 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004344 changes = [Changelist(branchref=b, auth_config=auth_config)
4345 for b in branches.splitlines()]
4346 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4347 statuses = get_cl_statuses(changes,
4348 fine_grained=True,
4349 max_processes=options.maxjobs)
4350 proposal = [(cl.GetBranch(),
4351 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4352 for cl, status in statuses
4353 if status == 'closed']
4354 proposal.sort()
4355
4356 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004358 return 0
4359
4360 current_branch = GetCurrentBranch()
4361
vapiera7fbd5a2016-06-16 09:17:49 -07004362 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004363 if options.notags:
4364 for next_item in proposal:
4365 print(' ' + next_item[0])
4366 else:
4367 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4368 for next_item in proposal:
4369 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004370
kmarshall9249e012016-08-23 12:02:16 -07004371 # Quit now on precondition failure or if instructed by the user, either
4372 # via an interactive prompt or by command line flags.
4373 if options.dry_run:
4374 print('\nNo changes were made (dry run).\n')
4375 return 0
4376 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004377 print('You are currently on a branch \'%s\' which is associated with a '
4378 'closed codereview issue, so archive cannot proceed. Please '
4379 'checkout another branch and run this command again.' %
4380 current_branch)
4381 return 1
kmarshall9249e012016-08-23 12:02:16 -07004382 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004383 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4384 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004386 return 1
4387
4388 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004389 if not options.notags:
4390 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004391 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004392
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004394
4395 return 0
4396
4397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004399 """Show status of changelists.
4400
4401 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004402 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004403 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004404 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004405 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004406 - Magenta in the commit queue
4407 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004408 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004409
4410 Also see 'git cl comments'.
4411 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004413 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004414 parser.add_option('-f', '--fast', action='store_true',
4415 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004416 parser.add_option(
4417 '-j', '--maxjobs', action='store', type=int,
4418 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004419
4420 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004421 _add_codereview_issue_select_options(
4422 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004423 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004424 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004425 if args:
4426 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004427 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004428
iannuccie53c9352016-08-17 14:40:40 -07004429 if options.issue is not None and not options.field:
4430 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004432 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004433 cl = Changelist(auth_config=auth_config, issue=options.issue,
4434 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004436 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437 elif options.field == 'id':
4438 issueid = cl.GetIssue()
4439 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004440 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004441 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004442 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004444 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004445 elif options.field == 'status':
4446 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004447 elif options.field == 'url':
4448 url = cl.GetIssueURL()
4449 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004451 return 0
4452
4453 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4454 if not branches:
4455 print('No local branch found.')
4456 return 0
4457
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004458 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004459 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004460 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004462 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004463 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004464 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004465
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004466 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004467 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4468 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4469 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004470 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004471 c, status = output.next()
4472 branch_statuses[c.GetBranch()] = status
4473 status = branch_statuses.pop(branch)
4474 url = cl.GetIssueURL()
4475 if url and (not status or status == 'error'):
4476 # The issue probably doesn't exist anymore.
4477 url += ' (broken)'
4478
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004479 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004480 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004481 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004482 color = ''
4483 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004484 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004485 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004486 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004487 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004488
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004489
4490 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004491 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004492 print('Current branch: %s' % branch)
4493 for cl in changes:
4494 if cl.GetBranch() == branch:
4495 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004496 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004497 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004498 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004500 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004501 print('Issue description:')
4502 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004503 return 0
4504
4505
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004506def colorize_CMDstatus_doc():
4507 """To be called once in main() to add colors to git cl status help."""
4508 colors = [i for i in dir(Fore) if i[0].isupper()]
4509
4510 def colorize_line(line):
4511 for color in colors:
4512 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004513 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004514 indent = len(line) - len(line.lstrip(' ')) + 1
4515 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4516 return line
4517
4518 lines = CMDstatus.__doc__.splitlines()
4519 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4520
4521
phajdan.jre328cf92016-08-22 04:12:17 -07004522def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004523 if path == '-':
4524 json.dump(contents, sys.stdout)
4525 else:
4526 with open(path, 'w') as f:
4527 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004528
4529
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004530@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004532 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533
4534 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004535 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004536 parser.add_option('-r', '--reverse', action='store_true',
4537 help='Lookup the branch(es) for the specified issues. If '
4538 'no issues are specified, all branches with mapped '
4539 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004540 parser.add_option('--json',
4541 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004542 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004543 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004544 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545
dnj@chromium.org406c4402015-03-03 17:22:28 +00004546 if options.reverse:
4547 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004548 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004549 # Reverse issue lookup.
4550 issue_branch_map = {}
4551 for branch in branches:
4552 cl = Changelist(branchref=branch)
4553 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4554 if not args:
4555 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004556 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004557 for issue in args:
4558 if not issue:
4559 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004560 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004561 print('Branch for issue number %s: %s' % (
4562 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004563 if options.json:
4564 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004565 return 0
4566
4567 if len(args) > 0:
4568 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4569 if not issue.valid:
4570 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4571 'or no argument to list it.\n'
4572 'Maybe you want to run git cl status?')
4573 cl = Changelist(codereview=issue.codereview)
4574 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004575 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004576 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004577 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4578 if options.json:
4579 write_json(options.json, {
4580 'issue': cl.GetIssue(),
4581 'issue_url': cl.GetIssueURL(),
4582 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004583 return 0
4584
4585
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004586def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004587 """Shows or posts review comments for any changelist."""
4588 parser.add_option('-a', '--add-comment', dest='comment',
4589 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004590 parser.add_option('-i', '--issue', dest='issue',
4591 help='review issue id (defaults to current issue). '
4592 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004593 parser.add_option('-m', '--machine-readable', dest='readable',
4594 action='store_false', default=True,
4595 help='output comments in a format compatible with '
4596 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004597 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004598 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004599 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004600 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004601 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004602 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004603 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004604
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004605 issue = None
4606 if options.issue:
4607 try:
4608 issue = int(options.issue)
4609 except ValueError:
4610 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004611 if not options.forced_codereview:
4612 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004613
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004614 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004615 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004616 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004617
4618 if options.comment:
4619 cl.AddComment(options.comment)
4620 return 0
4621
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004622 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4623 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004624 for comment in summary:
4625 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004626 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004627 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004628 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004629 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004630 color = Fore.MAGENTA
4631 else:
4632 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004633 print('\n%s%s %s%s\n%s' % (
4634 color,
4635 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4636 comment.sender,
4637 Fore.RESET,
4638 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4639
smut@google.comc85ac942015-09-15 16:34:43 +00004640 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004641 def pre_serialize(c):
4642 dct = c.__dict__.copy()
4643 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4644 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004645 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004646 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004647 return 0
4648
4649
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004650@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004651def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004652 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004653 parser.add_option('-d', '--display', action='store_true',
4654 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004655 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004656 help='New description to set for this issue (- for stdin, '
4657 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004658 parser.add_option('-f', '--force', action='store_true',
4659 help='Delete any unpublished Gerrit edits for this issue '
4660 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004661
4662 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004663 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004664 options, args = parser.parse_args(args)
4665 _process_codereview_select_options(parser, options)
4666
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004667 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004668 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004669 target_issue_arg = ParseIssueNumberArgument(args[0],
4670 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004671 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004672 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004673
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004674 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004675
martiniss6eda05f2016-06-30 10:18:35 -07004676 kwargs = {
4677 'auth_config': auth_config,
4678 'codereview': options.forced_codereview,
4679 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004680 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004681 if target_issue_arg:
4682 kwargs['issue'] = target_issue_arg.issue
4683 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004684 if target_issue_arg.codereview and not options.forced_codereview:
4685 detected_codereview_from_url = True
4686 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004687
4688 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004689 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004690 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004691 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004692
4693 if detected_codereview_from_url:
4694 logging.info('canonical issue/change URL: %s (type: %s)\n',
4695 cl.GetIssueURL(), target_issue_arg.codereview)
4696
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004697 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004698
smut@google.com34fb6b12015-07-13 20:03:26 +00004699 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004700 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004701 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004702
4703 if options.new_description:
4704 text = options.new_description
4705 if text == '-':
4706 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004707 elif text == '+':
4708 base_branch = cl.GetCommonAncestorWithUpstream()
4709 change = cl.GetChange(base_branch, None, local_description=True)
4710 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004711
4712 description.set_description(text)
4713 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004714 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004715
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004716 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004717 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004718 return 0
4719
4720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721def CreateDescriptionFromLog(args):
4722 """Pulls out the commit log to use as a base for the CL description."""
4723 log_args = []
4724 if len(args) == 1 and not args[0].endswith('.'):
4725 log_args = [args[0] + '..']
4726 elif len(args) == 1 and args[0].endswith('...'):
4727 log_args = [args[0][:-1]]
4728 elif len(args) == 2:
4729 log_args = [args[0] + '..' + args[1]]
4730 else:
4731 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004732 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004733
4734
thestig@chromium.org44202a22014-03-11 19:22:18 +00004735def CMDlint(parser, args):
4736 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004737 parser.add_option('--filter', action='append', metavar='-x,+y',
4738 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004739 auth.add_auth_options(parser)
4740 options, args = parser.parse_args(args)
4741 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004742
4743 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004744 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004745 try:
4746 import cpplint
4747 import cpplint_chromium
4748 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004749 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004750 return 1
4751
4752 # Change the current working directory before calling lint so that it
4753 # shows the correct base.
4754 previous_cwd = os.getcwd()
4755 os.chdir(settings.GetRoot())
4756 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004757 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004758 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4759 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004760 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004761 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004762 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004763
4764 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004765 command = args + files
4766 if options.filter:
4767 command = ['--filter=' + ','.join(options.filter)] + command
4768 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004769
4770 white_regex = re.compile(settings.GetLintRegex())
4771 black_regex = re.compile(settings.GetLintIgnoreRegex())
4772 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4773 for filename in filenames:
4774 if white_regex.match(filename):
4775 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004776 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004777 else:
4778 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4779 extra_check_functions)
4780 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004781 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004782 finally:
4783 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004784 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004785 if cpplint._cpplint_state.error_count != 0:
4786 return 1
4787 return 0
4788
4789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004790def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004791 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004792 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004793 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004794 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004795 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004796 parser.add_option('--all', action='store_true',
4797 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004798 parser.add_option('--parallel', action='store_true',
4799 help='Run all tests specified by input_api.RunTests in all '
4800 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004801 auth.add_auth_options(parser)
4802 options, args = parser.parse_args(args)
4803 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004804
sbc@chromium.org71437c02015-04-09 19:29:40 +00004805 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004806 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004807 return 1
4808
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004809 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810 if args:
4811 base_branch = args[0]
4812 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004813 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004814 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004815
Aaron Gable8076c282017-11-29 14:39:41 -08004816 if options.all:
4817 base_change = cl.GetChange(base_branch, None)
4818 files = [('M', f) for f in base_change.AllFiles()]
4819 change = presubmit_support.GitChange(
4820 base_change.Name(),
4821 base_change.FullDescriptionText(),
4822 base_change.RepositoryRoot(),
4823 files,
4824 base_change.issue,
4825 base_change.patchset,
4826 base_change.author_email,
4827 base_change._upstream)
4828 else:
4829 change = cl.GetChange(base_branch, None)
4830
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004831 cl.RunHook(
4832 committing=not options.upload,
4833 may_prompt=False,
4834 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004835 change=change,
4836 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004837 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004838
4839
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004840def GenerateGerritChangeId(message):
4841 """Returns Ixxxxxx...xxx change id.
4842
4843 Works the same way as
4844 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4845 but can be called on demand on all platforms.
4846
4847 The basic idea is to generate git hash of a state of the tree, original commit
4848 message, author/committer info and timestamps.
4849 """
4850 lines = []
4851 tree_hash = RunGitSilent(['write-tree'])
4852 lines.append('tree %s' % tree_hash.strip())
4853 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4854 if code == 0:
4855 lines.append('parent %s' % parent.strip())
4856 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4857 lines.append('author %s' % author.strip())
4858 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4859 lines.append('committer %s' % committer.strip())
4860 lines.append('')
4861 # Note: Gerrit's commit-hook actually cleans message of some lines and
4862 # whitespace. This code is not doing this, but it clearly won't decrease
4863 # entropy.
4864 lines.append(message)
4865 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4866 stdin='\n'.join(lines))
4867 return 'I%s' % change_hash.strip()
4868
4869
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004870def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004871 """Computes the remote branch ref to use for the CL.
4872
4873 Args:
4874 remote (str): The git remote for the CL.
4875 remote_branch (str): The git remote branch for the CL.
4876 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004877 """
4878 if not (remote and remote_branch):
4879 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004880
wittman@chromium.org455dc922015-01-26 20:15:50 +00004881 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004882 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004883 # refs, which are then translated into the remote full symbolic refs
4884 # below.
4885 if '/' not in target_branch:
4886 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4887 else:
4888 prefix_replacements = (
4889 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4890 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4891 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4892 )
4893 match = None
4894 for regex, replacement in prefix_replacements:
4895 match = re.search(regex, target_branch)
4896 if match:
4897 remote_branch = target_branch.replace(match.group(0), replacement)
4898 break
4899 if not match:
4900 # This is a branch path but not one we recognize; use as-is.
4901 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004902 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4903 # Handle the refs that need to land in different refs.
4904 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004905
wittman@chromium.org455dc922015-01-26 20:15:50 +00004906 # Create the true path to the remote branch.
4907 # Does the following translation:
4908 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4909 # * refs/remotes/origin/master -> refs/heads/master
4910 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4911 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4912 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4913 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4914 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4915 'refs/heads/')
4916 elif remote_branch.startswith('refs/remotes/branch-heads'):
4917 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004918
wittman@chromium.org455dc922015-01-26 20:15:50 +00004919 return remote_branch
4920
4921
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004922def cleanup_list(l):
4923 """Fixes a list so that comma separated items are put as individual items.
4924
4925 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4926 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4927 """
4928 items = sum((i.split(',') for i in l), [])
4929 stripped_items = (i.strip() for i in items)
4930 return sorted(filter(None, stripped_items))
4931
4932
Aaron Gable4db38df2017-11-03 14:59:07 -07004933@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004934def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004935 """Uploads the current changelist to codereview.
4936
4937 Can skip dependency patchset uploads for a branch by running:
4938 git config branch.branch_name.skip-deps-uploads True
4939 To unset run:
4940 git config --unset branch.branch_name.skip-deps-uploads
4941 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004942
4943 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4944 a bug number, this bug number is automatically populated in the CL
4945 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004946
4947 If subject contains text in square brackets or has "<text>: " prefix, such
4948 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4949 [git-cl] add support for hashtags
4950 Foo bar: implement foo
4951 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004952 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004953 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4954 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004955 parser.add_option('--bypass-watchlists', action='store_true',
4956 dest='bypass_watchlists',
4957 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004958 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004959 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004960 parser.add_option('--message', '-m', dest='message',
4961 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004962 parser.add_option('-b', '--bug',
4963 help='pre-populate the bug number(s) for this issue. '
4964 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004965 parser.add_option('--message-file', dest='message_file',
4966 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004967 parser.add_option('--title', '-t', dest='title',
4968 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004969 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004970 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004971 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004972 parser.add_option('--tbrs',
4973 action='append', default=[],
4974 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004975 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004976 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004977 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004978 parser.add_option('--hashtag', dest='hashtags',
4979 action='append', default=[],
4980 help=('Gerrit hashtag for new CL; '
4981 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004982 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004983 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004984 parser.add_option('--emulate_svn_auto_props',
4985 '--emulate-svn-auto-props',
4986 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004987 dest="emulate_svn_auto_props",
4988 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004989 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004990 help='tell the commit queue to commit this patchset; '
4991 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004992 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004993 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004994 metavar='TARGET',
4995 help='Apply CL to remote ref TARGET. ' +
4996 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004997 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004998 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004999 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005000 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005001 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005002 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005003 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5004 const='TBR', help='add a set of OWNERS to TBR')
5005 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5006 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005007 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5008 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005009 help='Send the patchset to do a CQ dry run right after '
5010 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005011 parser.add_option('--dependencies', action='store_true',
5012 help='Uploads CLs of all the local branches that depend on '
5013 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005014 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5015 help='Sends your change to the CQ after an approval. Only '
5016 'works on repos that have the Auto-Submit label '
5017 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005018 parser.add_option('--parallel', action='store_true',
5019 help='Run all tests specified by input_api.RunTests in all '
5020 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005021
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005022 # TODO: remove Rietveld flags
5023 parser.add_option('--private', action='store_true',
5024 help='set the review private (rietveld only)')
5025 parser.add_option('--email', default=None,
5026 help='email address to use to connect to Rietveld')
5027
rmistry@google.com2dd99862015-06-22 12:22:18 +00005028 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005029 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005030 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005031 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005032 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005033 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005034
sbc@chromium.org71437c02015-04-09 19:29:40 +00005035 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005036 return 1
5037
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005038 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005039 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005040 options.cc = cleanup_list(options.cc)
5041
tandriib80458a2016-06-23 12:20:07 -07005042 if options.message_file:
5043 if options.message:
5044 parser.error('only one of --message and --message-file allowed.')
5045 options.message = gclient_utils.FileRead(options.message_file)
5046 options.message_file = None
5047
tandrii4d0545a2016-07-06 03:56:49 -07005048 if options.cq_dry_run and options.use_commit_queue:
5049 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5050
Aaron Gableedbc4132017-09-11 13:22:28 -07005051 if options.use_commit_queue:
5052 options.send_mail = True
5053
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005054 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5055 settings.GetIsGerrit()
5056
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005057 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005058 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005059
5060
Francois Dorayd42c6812017-05-30 15:10:20 -04005061@subcommand.usage('--description=<description file>')
5062def CMDsplit(parser, args):
5063 """Splits a branch into smaller branches and uploads CLs.
5064
5065 Creates a branch and uploads a CL for each group of files modified in the
5066 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005067 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005068 the shared OWNERS file.
5069 """
5070 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005071 help="A text file containing a CL description in which "
5072 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005073 parser.add_option("-c", "--comment", dest="comment_file",
5074 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005075 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5076 default=False,
5077 help="List the files and reviewers for each CL that would "
5078 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005079 options, _ = parser.parse_args(args)
5080
5081 if not options.description_file:
5082 parser.error('No --description flag specified.')
5083
5084 def WrappedCMDupload(args):
5085 return CMDupload(OptionParser(), args)
5086
5087 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005088 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005089
5090
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005091@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005092def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005093 """DEPRECATED: Used to commit the current changelist via git-svn."""
5094 message = ('git-cl no longer supports committing to SVN repositories via '
5095 'git-svn. You probably want to use `git cl land` instead.')
5096 print(message)
5097 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005098
5099
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005100# Two special branches used by git cl land.
5101MERGE_BRANCH = 'git-cl-commit'
5102CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5103
5104
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005105@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005106def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005107 """Commits the current changelist via git.
5108
5109 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5110 upstream and closes the issue automatically and atomically.
5111
5112 Otherwise (in case of Rietveld):
5113 Squashes branch into a single commit.
5114 Updates commit message with metadata (e.g. pointer to review).
5115 Pushes the code upstream.
5116 Updates review and closes.
5117 """
5118 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5119 help='bypass upload presubmit hook')
5120 parser.add_option('-m', dest='message',
5121 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005122 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005123 help="force yes to questions (don't prompt)")
5124 parser.add_option('-c', dest='contributor',
5125 help="external contributor for patch (appended to " +
5126 "description and used as author for git). Should be " +
5127 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005128 parser.add_option('--parallel', action='store_true',
5129 help='Run all tests specified by input_api.RunTests in all '
5130 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005131 auth.add_auth_options(parser)
5132 (options, args) = parser.parse_args(args)
5133 auth_config = auth.extract_auth_config_from_options(options)
5134
5135 cl = Changelist(auth_config=auth_config)
5136
Robert Iannucci2e73d432018-03-14 01:10:47 -07005137 if not cl.IsGerrit():
5138 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005139
Robert Iannucci2e73d432018-03-14 01:10:47 -07005140 if options.message:
5141 # This could be implemented, but it requires sending a new patch to
5142 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5143 # Besides, Gerrit has the ability to change the commit message on submit
5144 # automatically, thus there is no need to support this option (so far?).
5145 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005146 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005147 parser.error(
5148 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5149 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5150 'the contributor\'s "name <email>". If you can\'t upload such a '
5151 'commit for review, contact your repository admin and request'
5152 '"Forge-Author" permission.')
5153 if not cl.GetIssue():
5154 DieWithError('You must upload the change first to Gerrit.\n'
5155 ' If you would rather have `git cl land` upload '
5156 'automatically for you, see http://crbug.com/642759')
5157 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005158 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005159
5160
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005161def PushToGitWithAutoRebase(remote, branch, original_description,
5162 git_numberer_enabled, max_attempts=3):
5163 """Pushes current HEAD commit on top of remote's branch.
5164
5165 Attempts to fetch and autorebase on push failures.
5166 Adds git number footers on the fly.
5167
5168 Returns integer code from last command.
5169 """
5170 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5171 code = 0
5172 attempts_left = max_attempts
5173 while attempts_left:
5174 attempts_left -= 1
5175 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5176
5177 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5178 # If fetch fails, retry.
5179 print('Fetching %s/%s...' % (remote, branch))
5180 code, out = RunGitWithCode(
5181 ['retry', 'fetch', remote,
5182 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5183 if code:
5184 print('Fetch failed with exit code %d.' % code)
5185 print(out.strip())
5186 continue
5187
5188 print('Cherry-picking commit on top of latest %s' % branch)
5189 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5190 suppress_stderr=True)
5191 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5192 code, out = RunGitWithCode(['cherry-pick', cherry])
5193 if code:
5194 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5195 'the following files have merge conflicts:' %
5196 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005197 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5198 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005199 print('Please rebase your patch and try again.')
5200 RunGitWithCode(['cherry-pick', '--abort'])
5201 break
5202
5203 commit_desc = ChangeDescription(original_description)
5204 if git_numberer_enabled:
5205 logging.debug('Adding git number footers')
5206 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5207 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5208 branch)
5209 # Ensure timestamps are monotonically increasing.
5210 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5211 _get_committer_timestamp('HEAD'))
5212 _git_amend_head(commit_desc.description, timestamp)
5213
5214 code, out = RunGitWithCode(
5215 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5216 print(out)
5217 if code == 0:
5218 break
5219 if IsFatalPushFailure(out):
5220 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005221 'user.email are correct and you have push access to the repo.\n'
5222 'Hint: run command below to diangose common Git/Gerrit credential '
5223 'problems:\n'
5224 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005225 break
5226 return code
5227
5228
5229def IsFatalPushFailure(push_stdout):
5230 """True if retrying push won't help."""
5231 return '(prohibited by Gerrit)' in push_stdout
5232
5233
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005234@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005235def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005236 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005237 parser.add_option('-b', dest='newbranch',
5238 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005239 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005240 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005241 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005242 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005243 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005244 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005245 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005246 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005247 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005248 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005249
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005250
5251 group = optparse.OptionGroup(
5252 parser,
5253 'Options for continuing work on the current issue uploaded from a '
5254 'different clone (e.g. different machine). Must be used independently '
5255 'from the other options. No issue number should be specified, and the '
5256 'branch must have an issue number associated with it')
5257 group.add_option('--reapply', action='store_true', dest='reapply',
5258 help='Reset the branch and reapply the issue.\n'
5259 'CAUTION: This will undo any local changes in this '
5260 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005261
5262 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005263 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005264 parser.add_option_group(group)
5265
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005266 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005267 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005268 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005269 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005270 auth_config = auth.extract_auth_config_from_options(options)
5271
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005272 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005273 if options.newbranch:
5274 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005275 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005276 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005277
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005278 cl = Changelist(auth_config=auth_config,
5279 codereview=options.forced_codereview)
5280 if not cl.GetIssue():
5281 parser.error('current branch must have an associated issue')
5282
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005283 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005284 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005285 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005286
5287 RunGit(['reset', '--hard', upstream])
5288 if options.pull:
5289 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005290
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005291 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5292 options.directory)
5293
5294 if len(args) != 1 or not args[0]:
5295 parser.error('Must specify issue number or url')
5296
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005297 target_issue_arg = ParseIssueNumberArgument(args[0],
5298 options.forced_codereview)
5299 if not target_issue_arg.valid:
5300 parser.error('invalid codereview url or CL id')
5301
5302 cl_kwargs = {
5303 'auth_config': auth_config,
5304 'codereview_host': target_issue_arg.hostname,
5305 'codereview': options.forced_codereview,
5306 }
5307 detected_codereview_from_url = False
5308 if target_issue_arg.codereview and not options.forced_codereview:
5309 detected_codereview_from_url = True
5310 cl_kwargs['codereview'] = target_issue_arg.codereview
5311 cl_kwargs['issue'] = target_issue_arg.issue
5312
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005313 # We don't want uncommitted changes mixed up with the patch.
5314 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005315 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005316
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005317 if options.newbranch:
5318 if options.force:
5319 RunGit(['branch', '-D', options.newbranch],
5320 stderr=subprocess2.PIPE, error_ok=True)
5321 RunGit(['new-branch', options.newbranch])
5322
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005323 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005324
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005325 if cl.IsGerrit():
5326 if options.reject:
5327 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005328 if options.directory:
5329 parser.error('--directory is not supported with Gerrit codereview.')
5330
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005331 if detected_codereview_from_url:
5332 print('canonical issue/change URL: %s (type: %s)\n' %
5333 (cl.GetIssueURL(), target_issue_arg.codereview))
5334
5335 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005336 options.nocommit, options.directory,
5337 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005338
5339
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005340def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005341 """Fetches the tree status and returns either 'open', 'closed',
5342 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005343 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005344 if url:
5345 status = urllib2.urlopen(url).read().lower()
5346 if status.find('closed') != -1 or status == '0':
5347 return 'closed'
5348 elif status.find('open') != -1 or status == '1':
5349 return 'open'
5350 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005351 return 'unset'
5352
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005354def GetTreeStatusReason():
5355 """Fetches the tree status from a json url and returns the message
5356 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005357 url = settings.GetTreeStatusUrl()
5358 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005359 connection = urllib2.urlopen(json_url)
5360 status = json.loads(connection.read())
5361 connection.close()
5362 return status['message']
5363
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005364
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005365def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005366 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005367 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005368 status = GetTreeStatus()
5369 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005370 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005371 return 2
5372
vapiera7fbd5a2016-06-16 09:17:49 -07005373 print('The tree is %s' % status)
5374 print()
5375 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005376 if status != 'open':
5377 return 1
5378 return 0
5379
5380
maruel@chromium.org15192402012-09-06 12:38:29 +00005381def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005382 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005383 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005384 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005385 '-b', '--bot', action='append',
5386 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5387 'times to specify multiple builders. ex: '
5388 '"-b win_rel -b win_layout". See '
5389 'the try server waterfall for the builders name and the tests '
5390 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005391 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005392 '-B', '--bucket', default='',
5393 help=('Buildbucket bucket to send the try requests.'))
5394 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005395 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005396 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005397 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005398 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005399 help='Revision to use for the try job; default: the revision will '
5400 'be determined by the try recipe that builder runs, which usually '
5401 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005402 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005403 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005404 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005405 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005406 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005407 '--category', default='git_cl_try', help='Specify custom build category.')
5408 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005409 '--project',
5410 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005411 'in recipe to determine to which repository or directory to '
5412 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005413 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005414 '-p', '--property', dest='properties', action='append', default=[],
5415 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005416 'key2=value2 etc. The value will be treated as '
5417 'json if decodable, or as string otherwise. '
5418 'NOTE: using this may make your try job not usable for CQ, '
5419 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005420 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005421 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5422 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005423 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005424 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005425 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005426 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005427 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005428 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005429
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005430 if options.master and options.master.startswith('luci.'):
5431 parser.error(
5432 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005433 # Make sure that all properties are prop=value pairs.
5434 bad_params = [x for x in options.properties if '=' not in x]
5435 if bad_params:
5436 parser.error('Got properties with missing "=": %s' % bad_params)
5437
maruel@chromium.org15192402012-09-06 12:38:29 +00005438 if args:
5439 parser.error('Unknown arguments: %s' % args)
5440
Koji Ishii31c14782018-01-08 17:17:33 +09005441 cl = Changelist(auth_config=auth_config, issue=options.issue,
5442 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005443 if not cl.GetIssue():
5444 parser.error('Need to upload first')
5445
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005446 if cl.IsGerrit():
5447 # HACK: warm up Gerrit change detail cache to save on RPCs.
5448 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5449
tandriie113dfd2016-10-11 10:20:12 -07005450 error_message = cl.CannotTriggerTryJobReason()
5451 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005452 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005453
borenet6c0efe62016-10-19 08:13:29 -07005454 if options.bucket and options.master:
5455 parser.error('Only one of --bucket and --master may be used.')
5456
qyearsley1fdfcb62016-10-24 13:22:03 -07005457 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005458
qyearsleydd49f942016-10-28 11:57:22 -07005459 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5460 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005461 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005462 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005463 print('git cl try with no bots now defaults to CQ dry run.')
5464 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5465 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005466
borenet6c0efe62016-10-19 08:13:29 -07005467 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005468 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005469 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005470 'of bot requires an initial job from a parent (usually a builder). '
5471 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005472 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005473 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005474
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005475 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005476 # TODO(tandrii): Checking local patchset against remote patchset is only
5477 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5478 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005479 print('Warning: Codereview server has newer patchsets (%s) than most '
5480 'recent upload from local checkout (%s). Did a previous upload '
5481 'fail?\n'
5482 'By default, git cl try uses the latest patchset from '
5483 'codereview, continuing to use patchset %s.\n' %
5484 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005485
tandrii568043b2016-10-11 07:49:18 -07005486 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005487 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005488 except BuildbucketResponseException as ex:
5489 print('ERROR: %s' % ex)
5490 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005491 return 0
5492
5493
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005494def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005495 """Prints info about try jobs associated with current CL."""
5496 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005497 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005498 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005499 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005500 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005501 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005502 '--color', action='store_true', default=setup_color.IS_TTY,
5503 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005504 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005505 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5506 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005507 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005508 '--json', help=('Path of JSON output file to write try job results to,'
5509 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005510 parser.add_option_group(group)
5511 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005512 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005513 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005514 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005515 if args:
5516 parser.error('Unrecognized args: %s' % ' '.join(args))
5517
5518 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005519 cl = Changelist(
5520 issue=options.issue, codereview=options.forced_codereview,
5521 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005522 if not cl.GetIssue():
5523 parser.error('Need to upload first')
5524
tandrii221ab252016-10-06 08:12:04 -07005525 patchset = options.patchset
5526 if not patchset:
5527 patchset = cl.GetMostRecentPatchset()
5528 if not patchset:
5529 parser.error('Codereview doesn\'t know about issue %s. '
5530 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005531 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005532 cl.GetIssue())
5533
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005534 # TODO(tandrii): Checking local patchset against remote patchset is only
5535 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5536 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005537 print('Warning: Codereview server has newer patchsets (%s) than most '
5538 'recent upload from local checkout (%s). Did a previous upload '
5539 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005540 'By default, git cl try-results uses the latest patchset from '
5541 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005542 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005543 try:
tandrii221ab252016-10-06 08:12:04 -07005544 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005545 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005546 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005547 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005548 if options.json:
5549 write_try_results_json(options.json, jobs)
5550 else:
5551 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005552 return 0
5553
5554
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005555@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005556def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005557 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005558 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005559 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005560 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005562 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005563 if args:
5564 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005565 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005566 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005567 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005568 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005569
5570 # Clear configured merge-base, if there is one.
5571 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005572 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005573 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005574 return 0
5575
5576
thestig@chromium.org00858c82013-12-02 23:08:03 +00005577def CMDweb(parser, args):
5578 """Opens the current CL in the web browser."""
5579 _, args = parser.parse_args(args)
5580 if args:
5581 parser.error('Unrecognized args: %s' % ' '.join(args))
5582
5583 issue_url = Changelist().GetIssueURL()
5584 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005585 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005586 return 1
5587
5588 webbrowser.open(issue_url)
5589 return 0
5590
5591
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005592def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005593 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005594 parser.add_option('-d', '--dry-run', action='store_true',
5595 help='trigger in dry run mode')
5596 parser.add_option('-c', '--clear', action='store_true',
5597 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005598 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005599 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005600 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005601 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005602 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005603 if args:
5604 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005605 if options.dry_run and options.clear:
5606 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5607
iannuccie53c9352016-08-17 14:40:40 -07005608 cl = Changelist(auth_config=auth_config, issue=options.issue,
5609 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005610 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005611 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005612 elif options.dry_run:
5613 state = _CQState.DRY_RUN
5614 else:
5615 state = _CQState.COMMIT
5616 if not cl.GetIssue():
5617 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005618 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005619 return 0
5620
5621
groby@chromium.org411034a2013-02-26 15:12:01 +00005622def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005623 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005624 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005625 auth.add_auth_options(parser)
5626 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005627 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005628 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005629 if args:
5630 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005631 cl = Changelist(auth_config=auth_config, issue=options.issue,
5632 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005633 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005634 if not cl.GetIssue():
5635 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005636 cl.CloseIssue()
5637 return 0
5638
5639
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005640def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005641 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005642 parser.add_option(
5643 '--stat',
5644 action='store_true',
5645 dest='stat',
5646 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005647 auth.add_auth_options(parser)
5648 options, args = parser.parse_args(args)
5649 auth_config = auth.extract_auth_config_from_options(options)
5650 if args:
5651 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005652
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005653 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005654 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005655 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005656 if not issue:
5657 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005658
Aaron Gablea718c3e2017-08-28 17:47:28 -07005659 base = cl._GitGetBranchConfigValue('last-upload-hash')
5660 if not base:
5661 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5662 if not base:
5663 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5664 revision_info = detail['revisions'][detail['current_revision']]
5665 fetch_info = revision_info['fetch']['http']
5666 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5667 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005668
Aaron Gablea718c3e2017-08-28 17:47:28 -07005669 cmd = ['git', 'diff']
5670 if options.stat:
5671 cmd.append('--stat')
5672 cmd.append(base)
5673 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005674
5675 return 0
5676
5677
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005678def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005679 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005680 parser.add_option(
5681 '--no-color',
5682 action='store_true',
5683 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005684 parser.add_option(
5685 '--batch',
5686 action='store_true',
5687 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005688 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005689 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005690 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005691
5692 author = RunGit(['config', 'user.email']).strip() or None
5693
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005694 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005695
5696 if args:
5697 if len(args) > 1:
5698 parser.error('Unknown args')
5699 base_branch = args[0]
5700 else:
5701 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005702 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005703
5704 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005705 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5706
5707 if options.batch:
5708 db = owners.Database(change.RepositoryRoot(), file, os.path)
5709 print('\n'.join(db.reviewers_for(affected_files, author)))
5710 return 0
5711
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005712 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005713 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005714 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005715 author,
5716 cl.GetReviewers(),
5717 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005718 disable_color=options.no_color,
5719 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005720
5721
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005722def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005723 """Generates a diff command."""
5724 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005725 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5726 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005727 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005728
5729 if args:
5730 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005731 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005732 diff_cmd.append(arg)
5733 else:
5734 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005735
5736 return diff_cmd
5737
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005738
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005739def MatchingFileType(file_name, extensions):
5740 """Returns true if the file name ends with one of the given extensions."""
5741 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005742
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005743
enne@chromium.org555cfe42014-01-29 18:21:39 +00005744@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005745def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005746 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005747 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005748 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005749 parser.add_option('--full', action='store_true',
5750 help='Reformat the full content of all touched files')
5751 parser.add_option('--dry-run', action='store_true',
5752 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005753 parser.add_option('--python', action='store_true',
5754 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005755 parser.add_option('--js', action='store_true',
5756 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005757 parser.add_option('--diff', action='store_true',
5758 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005759 parser.add_option('--presubmit', action='store_true',
5760 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005761 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005762
Daniel Chengc55eecf2016-12-30 03:11:02 -08005763 # Normalize any remaining args against the current path, so paths relative to
5764 # the current directory are still resolved as expected.
5765 args = [os.path.join(os.getcwd(), arg) for arg in args]
5766
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005767 # git diff generates paths against the root of the repository. Change
5768 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005769 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005770 if rel_base_path:
5771 os.chdir(rel_base_path)
5772
digit@chromium.org29e47272013-05-17 17:01:46 +00005773 # Grab the merge-base commit, i.e. the upstream commit of the current
5774 # branch when it was created or the last time it was rebased. This is
5775 # to cover the case where the user may have called "git fetch origin",
5776 # moving the origin branch to a newer commit, but hasn't rebased yet.
5777 upstream_commit = None
5778 cl = Changelist()
5779 upstream_branch = cl.GetUpstreamBranch()
5780 if upstream_branch:
5781 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5782 upstream_commit = upstream_commit.strip()
5783
5784 if not upstream_commit:
5785 DieWithError('Could not find base commit for this branch. '
5786 'Are you in detached state?')
5787
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005788 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5789 diff_output = RunGit(changed_files_cmd)
5790 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005791 # Filter out files deleted by this CL
5792 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005793
Christopher Lamc5ba6922017-01-24 11:19:14 +11005794 if opts.js:
5795 CLANG_EXTS.append('.js')
5796
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005797 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5798 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5799 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005800 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005801
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005802 top_dir = os.path.normpath(
5803 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5804
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005805 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5806 # formatted. This is used to block during the presubmit.
5807 return_value = 0
5808
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005809 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005810 # Locate the clang-format binary in the checkout
5811 try:
5812 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005813 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005814 DieWithError(e)
5815
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005816 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005817 cmd = [clang_format_tool]
5818 if not opts.dry_run and not opts.diff:
5819 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005820 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005821 if opts.diff:
5822 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005823 else:
5824 env = os.environ.copy()
5825 env['PATH'] = str(os.path.dirname(clang_format_tool))
5826 try:
5827 script = clang_format.FindClangFormatScriptInChromiumTree(
5828 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005829 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005830 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005831
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005832 cmd = [sys.executable, script, '-p0']
5833 if not opts.dry_run and not opts.diff:
5834 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005835
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005836 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5837 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005838
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005839 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5840 if opts.diff:
5841 sys.stdout.write(stdout)
5842 if opts.dry_run and len(stdout) > 0:
5843 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005844
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005845 # Similar code to above, but using yapf on .py files rather than clang-format
5846 # on C/C++ files
5847 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005848 yapf_tool = gclient_utils.FindExecutable('yapf')
5849 if yapf_tool is None:
5850 DieWithError('yapf not found in PATH')
5851
5852 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005853 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005854 cmd = [yapf_tool]
5855 if not opts.dry_run and not opts.diff:
5856 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005857 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005858 if opts.diff:
5859 sys.stdout.write(stdout)
5860 else:
5861 # TODO(sbc): yapf --lines mode still has some issues.
5862 # https://github.com/google/yapf/issues/154
5863 DieWithError('--python currently only works with --full')
5864
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005865 # Dart's formatter does not have the nice property of only operating on
5866 # modified chunks, so hard code full.
5867 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005868 try:
5869 command = [dart_format.FindDartFmtToolInChromiumTree()]
5870 if not opts.dry_run and not opts.diff:
5871 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005872 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005873
ppi@chromium.org6593d932016-03-03 15:41:15 +00005874 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005875 if opts.dry_run and stdout:
5876 return_value = 2
5877 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005878 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5879 'found in this checkout. Files in other languages are still '
5880 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005881
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005882 # Format GN build files. Always run on full build files for canonical form.
5883 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005884 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005885 if opts.dry_run or opts.diff:
5886 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005887 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005888 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5889 shell=sys.platform == 'win32',
5890 cwd=top_dir)
5891 if opts.dry_run and gn_ret == 2:
5892 return_value = 2 # Not formatted.
5893 elif opts.diff and gn_ret == 2:
5894 # TODO this should compute and print the actual diff.
5895 print("This change has GN build file diff for " + gn_diff_file)
5896 elif gn_ret != 0:
5897 # For non-dry run cases (and non-2 return values for dry-run), a
5898 # nonzero error code indicates a failure, probably because the file
5899 # doesn't parse.
5900 DieWithError("gn format failed on " + gn_diff_file +
5901 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005902
Ilya Shermane081cbe2017-08-15 17:51:04 -07005903 # Skip the metrics formatting from the global presubmit hook. These files have
5904 # a separate presubmit hook that issues an error if the files need formatting,
5905 # whereas the top-level presubmit script merely issues a warning. Formatting
5906 # these files is somewhat slow, so it's important not to duplicate the work.
5907 if not opts.presubmit:
5908 for xml_dir in GetDirtyMetricsDirs(diff_files):
5909 tool_dir = os.path.join(top_dir, xml_dir)
5910 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5911 if opts.dry_run or opts.diff:
5912 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005913 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005914 if opts.diff:
5915 sys.stdout.write(stdout)
5916 if opts.dry_run and stdout:
5917 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005918
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005919 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005920
Steven Holte2e664bf2017-04-21 13:10:47 -07005921def GetDirtyMetricsDirs(diff_files):
5922 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5923 metrics_xml_dirs = [
5924 os.path.join('tools', 'metrics', 'actions'),
5925 os.path.join('tools', 'metrics', 'histograms'),
5926 os.path.join('tools', 'metrics', 'rappor'),
5927 os.path.join('tools', 'metrics', 'ukm')]
5928 for xml_dir in metrics_xml_dirs:
5929 if any(file.startswith(xml_dir) for file in xml_diff_files):
5930 yield xml_dir
5931
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005932
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005933@subcommand.usage('<codereview url or issue id>')
5934def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005935 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005936 _, args = parser.parse_args(args)
5937
5938 if len(args) != 1:
5939 parser.print_help()
5940 return 1
5941
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005942 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005943 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005944 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005945
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005946 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005947
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005948 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005949 output = RunGit(['config', '--local', '--get-regexp',
5950 r'branch\..*\.%s' % issueprefix],
5951 error_ok=True)
5952 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005953 if issue == target_issue:
5954 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005955
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005956 branches = []
5957 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005958 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005959 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005960 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005961 return 1
5962 if len(branches) == 1:
5963 RunGit(['checkout', branches[0]])
5964 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005965 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005966 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005967 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005968 which = raw_input('Choose by index: ')
5969 try:
5970 RunGit(['checkout', branches[int(which)]])
5971 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005972 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005973 return 1
5974
5975 return 0
5976
5977
maruel@chromium.org29404b52014-09-08 22:58:00 +00005978def CMDlol(parser, args):
5979 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005980 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005981 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5982 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5983 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005984 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005985 return 0
5986
5987
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005988class OptionParser(optparse.OptionParser):
5989 """Creates the option parse and add --verbose support."""
5990 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005991 optparse.OptionParser.__init__(
5992 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005993 self.add_option(
5994 '-v', '--verbose', action='count', default=0,
5995 help='Use 2 times for more debugging info')
5996
5997 def parse_args(self, args=None, values=None):
5998 options, args = optparse.OptionParser.parse_args(self, args, values)
5999 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006000 logging.basicConfig(
6001 level=levels[min(options.verbose, len(levels) - 1)],
6002 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6003 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006004 return options, args
6005
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006007def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006008 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006009 print('\nYour python version %s is unsupported, please upgrade.\n' %
6010 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006011 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006012
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006013 # Reload settings.
6014 global settings
6015 settings = Settings()
6016
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006017 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006018 dispatcher = subcommand.CommandDispatcher(__name__)
6019 try:
6020 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006021 except auth.AuthenticationError as e:
6022 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006023 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006024 if e.code != 500:
6025 raise
6026 DieWithError(
6027 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6028 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006029 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006030
6031
6032if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006033 # These affect sys.stdout so do it outside of main() to simplify mocks in
6034 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006035 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006036 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006037 try:
6038 sys.exit(main(sys.argv[1:]))
6039 except KeyboardInterrupt:
6040 sys.stderr.write('interrupted\n')
6041 sys.exit(1)