blob: a0905b720f651d95981f9a45903c7cdb249824d3 [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'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700357 'Please file bugs at http://crbug.com, '
358 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 content)
360 return content_json
361 if response.status < 500 or try_count >= 2:
362 raise httplib2.HttpLib2Error(content)
363
364 # status >= 500 means transient failures.
365 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700366 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000367 try_count += 1
368 assert False, 'unreachable'
369
370
qyearsley1fdfcb62016-10-24 13:22:03 -0700371def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700372 """Returns a dict mapping bucket names to builders and tests,
373 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700374 """
qyearsleydd49f942016-10-28 11:57:22 -0700375 # If no bots are listed, we try to get a set of builders and tests based
376 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700377 if not options.bot:
378 change = changelist.GetChange(
379 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700380 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700381 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700382 change=change,
383 changed_files=change.LocalPaths(),
384 repository_root=settings.GetRoot(),
385 default_presubmit=None,
386 project=None,
387 verbose=options.verbose,
388 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700389 if masters is None:
390 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100391 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700392
qyearsley1fdfcb62016-10-24 13:22:03 -0700393 if options.bucket:
394 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700395 if options.master:
396 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700397
qyearsleydd49f942016-10-28 11:57:22 -0700398 # If bots are listed but no master or bucket, then we need to find out
399 # the corresponding master for each bot.
400 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
401 if error_message:
402 option_parser.error(
403 'Tryserver master cannot be found because: %s\n'
404 'Please manually specify the tryserver master, e.g. '
405 '"-m tryserver.chromium.linux".' % error_message)
406 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700407
408
qyearsley123a4682016-10-26 09:12:17 -0700409def _get_bucket_map_for_builders(builders):
410 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 map_url = 'https://builders-map.appspot.com/'
412 try:
qyearsley123a4682016-10-26 09:12:17 -0700413 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 except urllib2.URLError as e:
415 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
416 (map_url, e))
417 except ValueError as e:
418 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700419 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 return None, 'Failed to build master map.'
421
qyearsley123a4682016-10-26 09:12:17 -0700422 bucket_map = {}
423 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800424 bucket = builders_map.get(builder, {}).get('bucket')
425 if bucket:
426 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700427 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700428
429
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800430def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700431 """Sends a request to Buildbucket to trigger try jobs for a changelist.
432
433 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700434 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 changelist: Changelist that the try jobs are associated with.
436 buckets: A nested dict mapping bucket names to builders to tests.
437 options: Command-line options.
438 """
tandriide281ae2016-10-12 06:02:30 -0700439 assert changelist.GetIssue(), 'CL must be uploaded first'
440 codereview_url = changelist.GetCodereviewServer()
441 assert codereview_url, 'CL must be uploaded first'
442 patchset = patchset or changelist.GetMostRecentPatchset()
443 assert patchset, 'CL must be uploaded first'
444
445 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700446 # Cache the buildbucket credentials under the codereview host key, so that
447 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700448 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000449 http = authenticator.authorize(httplib2.Http())
450 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700451
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000452 buildbucket_put_url = (
453 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000454 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700455 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
456 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
457 hostname=codereview_host,
458 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700460
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700461 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800462 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700463 if options.clobber:
464 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700465 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700466 if extra_properties:
467 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468
469 batch_req_body = {'builds': []}
470 print_text = []
471 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700472 for bucket, builders_and_tests in sorted(buckets.iteritems()):
473 print_text.append('Bucket: %s' % bucket)
474 master = None
475 if bucket.startswith(MASTER_PREFIX):
476 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477 for builder, tests in sorted(builders_and_tests.iteritems()):
478 print_text.append(' %s: %s' % (builder, tests))
479 parameters = {
480 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000481 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100482 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000483 'revision': options.revision,
484 }],
tandrii8c5a3532016-11-04 07:52:02 -0700485 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000487 if 'presubmit' in builder.lower():
488 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000489 if tests:
490 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700491
492 tags = [
493 'builder:%s' % builder,
494 'buildset:%s' % buildset,
495 'user_agent:git_cl_try',
496 ]
497 if master:
498 parameters['properties']['master'] = master
499 tags.append('master:%s' % master)
500
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000501 batch_req_body['builds'].append(
502 {
503 'bucket': bucket,
504 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700506 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 }
508 )
509
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700511 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 http,
513 buildbucket_put_url,
514 'PUT',
515 body=json.dumps(batch_req_body),
516 headers={'Content-Type': 'application/json'}
517 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000518 print_text.append('To see results here, run: git cl try-results')
519 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700520 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000521
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000522
tandrii221ab252016-10-06 08:12:04 -0700523def fetch_try_jobs(auth_config, changelist, buildbucket_host,
524 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700525 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526
qyearsley53f48a12016-09-01 10:45:13 -0700527 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528 """
tandrii221ab252016-10-06 08:12:04 -0700529 assert buildbucket_host
530 assert changelist.GetIssue(), 'CL must be uploaded first'
531 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
532 patchset = patchset or changelist.GetMostRecentPatchset()
533 assert patchset, 'CL must be uploaded first'
534
535 codereview_url = changelist.GetCodereviewServer()
536 codereview_host = urlparse.urlparse(codereview_url).hostname
537 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538 if authenticator.has_cached_credentials():
539 http = authenticator.authorize(httplib2.Http())
540 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700541 print('Warning: Some results might be missing because %s' %
542 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700543 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 http = httplib2.Http()
545
546 http.force_exception_to_status_code = True
547
tandrii221ab252016-10-06 08:12:04 -0700548 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
549 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
550 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700552 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 params = {'tag': 'buildset:%s' % buildset}
554
555 builds = {}
556 while True:
557 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700558 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700560 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000561 for build in content.get('builds', []):
562 builds[build['id']] = build
563 if 'next_cursor' in content:
564 params['start_cursor'] = content['next_cursor']
565 else:
566 break
567 return builds
568
569
qyearsleyeab3c042016-08-24 09:18:28 -0700570def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 """Prints nicely result of fetch_try_jobs."""
572 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700573 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574 return
575
576 # Make a copy, because we'll be modifying builds dictionary.
577 builds = builds.copy()
578 builder_names_cache = {}
579
580 def get_builder(b):
581 try:
582 return builder_names_cache[b['id']]
583 except KeyError:
584 try:
585 parameters = json.loads(b['parameters_json'])
586 name = parameters['builder_name']
587 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700588 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700589 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 name = None
591 builder_names_cache[b['id']] = name
592 return name
593
594 def get_bucket(b):
595 bucket = b['bucket']
596 if bucket.startswith('master.'):
597 return bucket[len('master.'):]
598 return bucket
599
600 if options.print_master:
601 name_fmt = '%%-%ds %%-%ds' % (
602 max(len(str(get_bucket(b))) for b in builds.itervalues()),
603 max(len(str(get_builder(b))) for b in builds.itervalues()))
604 def get_name(b):
605 return name_fmt % (get_bucket(b), get_builder(b))
606 else:
607 name_fmt = '%%-%ds' % (
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % get_builder(b)
611
612 def sort_key(b):
613 return b['status'], b.get('result'), get_name(b), b.get('url')
614
615 def pop(title, f, color=None, **kwargs):
616 """Pop matching builds from `builds` dict and print them."""
617
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000618 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000619 colorize = str
620 else:
621 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
622
623 result = []
624 for b in builds.values():
625 if all(b.get(k) == v for k, v in kwargs.iteritems()):
626 builds.pop(b['id'])
627 result.append(b)
628 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700629 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700631 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000632
633 total = len(builds)
634 pop(status='COMPLETED', result='SUCCESS',
635 title='Successes:', color=Fore.GREEN,
636 f=lambda b: (get_name(b), b.get('url')))
637 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
638 title='Infra Failures:', color=Fore.MAGENTA,
639 f=lambda b: (get_name(b), b.get('url')))
640 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
641 title='Failures:', color=Fore.RED,
642 f=lambda b: (get_name(b), b.get('url')))
643 pop(status='COMPLETED', result='CANCELED',
644 title='Canceled:', color=Fore.MAGENTA,
645 f=lambda b: (get_name(b),))
646 pop(status='COMPLETED', result='FAILURE',
647 failure_reason='INVALID_BUILD_DEFINITION',
648 title='Wrong master/builder name:', color=Fore.MAGENTA,
649 f=lambda b: (get_name(b),))
650 pop(status='COMPLETED', result='FAILURE',
651 title='Other failures:',
652 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
653 pop(status='COMPLETED',
654 title='Other finished:',
655 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
656 pop(status='STARTED',
657 title='Started:', color=Fore.YELLOW,
658 f=lambda b: (get_name(b), b.get('url')))
659 pop(status='SCHEDULED',
660 title='Scheduled:',
661 f=lambda b: (get_name(b), 'id=%s' % b['id']))
662 # The last section is just in case buildbucket API changes OR there is a bug.
663 pop(title='Other:',
664 f=lambda b: (get_name(b), 'id=%s' % b['id']))
665 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700666 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000667
668
qyearsley53f48a12016-09-01 10:45:13 -0700669def write_try_results_json(output_file, builds):
670 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
671
672 The input |builds| dict is assumed to be generated by Buildbucket.
673 Buildbucket documentation: http://goo.gl/G0s101
674 """
675
676 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800677 """Extracts some of the information from one build dict."""
678 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700679 return {
680 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700681 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800682 'builder_name': parameters.get('builder_name'),
683 'created_ts': build.get('created_ts'),
684 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700685 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800686 'result': build.get('result'),
687 'status': build.get('status'),
688 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700689 'url': build.get('url'),
690 }
691
692 converted = []
693 for _, build in sorted(builds.items()):
694 converted.append(convert_build_dict(build))
695 write_json(output_file, converted)
696
697
Aaron Gable13101a62018-02-09 13:20:41 -0800698def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000699 """Prints statistics about the change to the user."""
700 # --no-ext-diff is broken in some versions of Git, so try to work around
701 # this by overriding the environment (but there is still a problem if the
702 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000703 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000704 if 'GIT_EXTERNAL_DIFF' in env:
705 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000706
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000707 try:
708 stdout = sys.stdout.fileno()
709 except AttributeError:
710 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000711 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800712 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000713 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000714
715
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000716class BuildbucketResponseException(Exception):
717 pass
718
719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720class Settings(object):
721 def __init__(self):
722 self.default_server = None
723 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000724 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 self.tree_status_url = None
726 self.viewvc_url = None
727 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000728 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000729 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000730 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000731 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000732 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000733 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734
735 def LazyUpdateIfNeeded(self):
736 """Updates the settings from a codereview.settings file, if available."""
737 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000738 # The only value that actually changes the behavior is
739 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000740 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000741 error_ok=True
742 ).strip().lower()
743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000745 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 LoadCodereviewSettingsFromFile(cr_settings_file)
747 self.updated = True
748
749 def GetDefaultServerUrl(self, error_ok=False):
750 if not self.default_server:
751 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000752 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000753 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if error_ok:
755 return self.default_server
756 if not self.default_server:
757 error_message = ('Could not find settings file. You must configure '
758 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000759 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761 return self.default_server
762
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000763 @staticmethod
764 def GetRelativeRoot():
765 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000768 if self.root is None:
769 self.root = os.path.abspath(self.GetRelativeRoot())
770 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000772 def GetGitMirror(self, remote='origin'):
773 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000774 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000775 if not os.path.isdir(local_url):
776 return None
777 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
778 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100779 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100780 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000781 if mirror.exists():
782 return mirror
783 return None
784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 def GetTreeStatusUrl(self, error_ok=False):
786 if not self.tree_status_url:
787 error_message = ('You must configure your tree status URL by running '
788 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789 self.tree_status_url = self._GetRietveldConfig(
790 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 return self.tree_status_url
792
793 def GetViewVCUrl(self):
794 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 return self.viewvc_url
797
rmistry@google.com90752582014-01-14 21:04:50 +0000798 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000799 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000800
rmistry@google.com78948ed2015-07-08 23:09:57 +0000801 def GetIsSkipDependencyUpload(self, branch_name):
802 """Returns true if specified branch should skip dep uploads."""
803 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
804 error_ok=True)
805
rmistry@google.com5626a922015-02-26 14:03:30 +0000806 def GetRunPostUploadHook(self):
807 run_post_upload_hook = self._GetRietveldConfig(
808 'run-post-upload-hook', error_ok=True)
809 return run_post_upload_hook == "True"
810
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000811 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000813
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000814 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000815 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000816
ukai@chromium.orge8077812012-02-03 03:41:46 +0000817 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700818 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000819 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700820 self.is_gerrit = (
821 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000822 return self.is_gerrit
823
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000824 def GetSquashGerritUploads(self):
825 """Return true if uploads to Gerrit should be squashed by default."""
826 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700827 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
828 if self.squash_gerrit_uploads is None:
829 # Default is squash now (http://crbug.com/611892#c23).
830 self.squash_gerrit_uploads = not (
831 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
832 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 return self.squash_gerrit_uploads
834
tandriia60502f2016-06-20 02:01:53 -0700835 def GetSquashGerritUploadsOverride(self):
836 """Return True or False if codereview.settings should be overridden.
837
838 Returns None if no override has been defined.
839 """
840 # See also http://crbug.com/611892#c23
841 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
842 error_ok=True).strip()
843 if result == 'true':
844 return True
845 if result == 'false':
846 return False
847 return None
848
tandrii@chromium.org28253532016-04-14 13:46:56 +0000849 def GetGerritSkipEnsureAuthenticated(self):
850 """Return True if EnsureAuthenticated should not be done for Gerrit
851 uploads."""
852 if self.gerrit_skip_ensure_authenticated is None:
853 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000854 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000855 error_ok=True).strip() == 'true')
856 return self.gerrit_skip_ensure_authenticated
857
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000858 def GetGitEditor(self):
859 """Return the editor specified in the git config, or None if none is."""
860 if self.git_editor is None:
861 self.git_editor = self._GetConfig('core.editor', error_ok=True)
862 return self.git_editor or None
863
thestig@chromium.org44202a22014-03-11 19:22:18 +0000864 def GetLintRegex(self):
865 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
866 DEFAULT_LINT_REGEX)
867
868 def GetLintIgnoreRegex(self):
869 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
870 DEFAULT_LINT_IGNORE_REGEX)
871
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000872 def GetProject(self):
873 if not self.project:
874 self.project = self._GetRietveldConfig('project', error_ok=True)
875 return self.project
876
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000877 def _GetRietveldConfig(self, param, **kwargs):
878 return self._GetConfig('rietveld.' + param, **kwargs)
879
rmistry@google.com78948ed2015-07-08 23:09:57 +0000880 def _GetBranchConfig(self, branch_name, param, **kwargs):
881 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
882
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 def _GetConfig(self, param, **kwargs):
884 self.LazyUpdateIfNeeded()
885 return RunGit(['config', param], **kwargs).strip()
886
887
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100888@contextlib.contextmanager
889def _get_gerrit_project_config_file(remote_url):
890 """Context manager to fetch and store Gerrit's project.config from
891 refs/meta/config branch and store it in temp file.
892
893 Provides a temporary filename or None if there was error.
894 """
895 error, _ = RunGitWithCode([
896 'fetch', remote_url,
897 '+refs/meta/config:refs/git_cl/meta/config'])
898 if error:
899 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700900 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100901 (remote_url, error))
902 yield None
903 return
904
905 error, project_config_data = RunGitWithCode(
906 ['show', 'refs/git_cl/meta/config:project.config'])
907 if error:
908 print('WARNING: project.config file not found')
909 yield None
910 return
911
912 with gclient_utils.temporary_directory() as tempdir:
913 project_config_file = os.path.join(tempdir, 'project.config')
914 gclient_utils.FileWrite(project_config_file, project_config_data)
915 yield project_config_file
916
917
918def _is_git_numberer_enabled(remote_url, remote_ref):
919 """Returns True if Git Numberer is enabled on this ref."""
920 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100921 KNOWN_PROJECTS_WHITELIST = [
922 'chromium/src',
923 'external/webrtc',
924 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100925 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200926 # For webrtc.googlesource.com/src.
927 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100928 ]
929
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100930 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
931 url_parts = urlparse.urlparse(remote_url)
932 project_name = url_parts.path.lstrip('/').rstrip('git./')
933 for known in KNOWN_PROJECTS_WHITELIST:
934 if project_name.endswith(known):
935 break
936 else:
937 # Early exit to avoid extra fetches for repos that aren't using Git
938 # Numberer.
939 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100940
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100941 with _get_gerrit_project_config_file(remote_url) as project_config_file:
942 if project_config_file is None:
943 # Failed to fetch project.config, which shouldn't happen on open source
944 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100945 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100946 def get_opts(x):
947 code, out = RunGitWithCode(
948 ['config', '-f', project_config_file, '--get-all',
949 'plugin.git-numberer.validate-%s-refglob' % x])
950 if code == 0:
951 return out.strip().splitlines()
952 return []
953 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100954
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100955 logging.info('validator config enabled %s disabled %s refglobs for '
956 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000957
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100959 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100960 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100961 return True
962 return False
963
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100964 if match_refglobs(disabled):
965 return False
966 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100967
968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969def ShortBranchName(branch):
970 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000971 return branch.replace('refs/heads/', '', 1)
972
973
974def GetCurrentBranchRef():
975 """Returns branch ref (e.g., refs/heads/master) or None."""
976 return RunGit(['symbolic-ref', 'HEAD'],
977 stderr=subprocess2.VOID, error_ok=True).strip() or None
978
979
980def GetCurrentBranch():
981 """Returns current branch or None.
982
983 For refs/heads/* branches, returns just last part. For others, full ref.
984 """
985 branchref = GetCurrentBranchRef()
986 if branchref:
987 return ShortBranchName(branchref)
988 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000991class _CQState(object):
992 """Enum for states of CL with respect to Commit Queue."""
993 NONE = 'none'
994 DRY_RUN = 'dry_run'
995 COMMIT = 'commit'
996
997 ALL_STATES = [NONE, DRY_RUN, COMMIT]
998
999
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001000class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001001 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001002 self.issue = issue
1003 self.patchset = patchset
1004 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001005 assert codereview in (None, 'rietveld', 'gerrit')
1006 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001007
1008 @property
1009 def valid(self):
1010 return self.issue is not None
1011
1012
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001013def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001014 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1015 fail_result = _ParsedIssueNumberArgument()
1016
1017 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001018 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001019 if not arg.startswith('http'):
1020 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001021
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001022 url = gclient_utils.UpgradeToHttps(arg)
1023 try:
1024 parsed_url = urlparse.urlparse(url)
1025 except ValueError:
1026 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001027
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001028 if codereview is not None:
1029 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1030 return parsed or fail_result
1031
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001032 results = {}
1033 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1034 parsed = cls.ParseIssueURL(parsed_url)
1035 if parsed is not None:
1036 results[name] = parsed
1037
1038 if not results:
1039 return fail_result
1040 if len(results) == 1:
1041 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001042
1043 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1044 # This is likely Gerrit.
1045 return results['gerrit']
1046 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001047 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001048
1049
Aaron Gablea45ee112016-11-22 15:14:38 -08001050class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001051 def __init__(self, issue, url):
1052 self.issue = issue
1053 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001054 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001055
1056 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001057 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001058 self.issue, self.url)
1059
1060
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001061_CommentSummary = collections.namedtuple(
1062 '_CommentSummary', ['date', 'message', 'sender',
1063 # TODO(tandrii): these two aren't known in Gerrit.
1064 'approval', 'disapproval'])
1065
1066
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001068 """Changelist works with one changelist in local branch.
1069
1070 Supports two codereview backends: Rietveld or Gerrit, selected at object
1071 creation.
1072
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001073 Notes:
1074 * Not safe for concurrent multi-{thread,process} use.
1075 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001076 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001077 """
1078
1079 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1080 """Create a new ChangeList instance.
1081
1082 If issue is given, the codereview must be given too.
1083
1084 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1085 Otherwise, it's decided based on current configuration of the local branch,
1086 with default being 'rietveld' for backwards compatibility.
1087 See _load_codereview_impl for more details.
1088
1089 **kwargs will be passed directly to codereview implementation.
1090 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001092 global settings
1093 if not settings:
1094 # Happens when git_cl.py is used as a utility library.
1095 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001096
1097 if issue:
1098 assert codereview, 'codereview must be known, if issue is known'
1099
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.branchref = branchref
1101 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001102 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.branch = ShortBranchName(self.branchref)
1104 else:
1105 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001107 self.lookedup_issue = False
1108 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.has_description = False
1110 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001111 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001113 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001114 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001116
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001118 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001120 assert self._codereview_impl
1121 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001122
1123 def _load_codereview_impl(self, codereview=None, **kwargs):
1124 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001125 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1126 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1127 self._codereview = codereview
1128 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001129 return
1130
1131 # Automatic selection based on issue number set for a current branch.
1132 # Rietveld takes precedence over Gerrit.
1133 assert not self.issue
1134 # Whether we find issue or not, we are doing the lookup.
1135 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001136 if self.GetBranch():
1137 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1138 issue = _git_get_branch_config_value(
1139 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1140 if issue:
1141 self._codereview = codereview
1142 self._codereview_impl = cls(self, **kwargs)
1143 self.issue = int(issue)
1144 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145
1146 # No issue is set for this branch, so decide based on repo-wide settings.
1147 return self._load_codereview_impl(
1148 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1149 **kwargs)
1150
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 def IsGerrit(self):
1152 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153
1154 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001155 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001156
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001157 The return value is a string suitable for passing to git cl with the --cc
1158 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001159 """
1160 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001161 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001162 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001163 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1164 return self.cc
1165
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001166 def GetCCListWithoutDefault(self):
1167 """Return the users cc'd on this CL excluding default ones."""
1168 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001169 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001170 return self.cc
1171
Daniel Cheng7227d212017-11-17 08:12:37 -08001172 def ExtendCC(self, more_cc):
1173 """Extends the list of users to cc on this CL based on the changed files."""
1174 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
1176 def GetBranch(self):
1177 """Returns the short branch name, e.g. 'master'."""
1178 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001179 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001180 if not branchref:
1181 return None
1182 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 self.branch = ShortBranchName(self.branchref)
1184 return self.branch
1185
1186 def GetBranchRef(self):
1187 """Returns the full branch name, e.g. 'refs/heads/master'."""
1188 self.GetBranch() # Poke the lazy loader.
1189 return self.branchref
1190
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001191 def ClearBranch(self):
1192 """Clears cached branch data of this object."""
1193 self.branch = self.branchref = None
1194
tandrii5d48c322016-08-18 16:19:37 -07001195 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1196 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1197 kwargs['branch'] = self.GetBranch()
1198 return _git_get_branch_config_value(key, default, **kwargs)
1199
1200 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1201 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1202 assert self.GetBranch(), (
1203 'this CL must have an associated branch to %sset %s%s' %
1204 ('un' if value is None else '',
1205 key,
1206 '' if value is None else ' to %r' % value))
1207 kwargs['branch'] = self.GetBranch()
1208 return _git_set_branch_config_value(key, value, **kwargs)
1209
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001210 @staticmethod
1211 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001212 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 e.g. 'origin', 'refs/heads/master'
1214 """
1215 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001216 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001219 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001221 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1222 error_ok=True).strip()
1223 if upstream_branch:
1224 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001226 # Else, try to guess the origin remote.
1227 remote_branches = RunGit(['branch', '-r']).split()
1228 if 'origin/master' in remote_branches:
1229 # Fall back on origin/master if it exits.
1230 remote = 'origin'
1231 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001233 DieWithError(
1234 'Unable to determine default branch to diff against.\n'
1235 'Either pass complete "git diff"-style arguments, like\n'
1236 ' git cl upload origin/master\n'
1237 'or verify this branch is set up to track another \n'
1238 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
1240 return remote, upstream_branch
1241
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001242 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001243 upstream_branch = self.GetUpstreamBranch()
1244 if not BranchExists(upstream_branch):
1245 DieWithError('The upstream for the current branch (%s) does not exist '
1246 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001247 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001248 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 def GetUpstreamBranch(self):
1251 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001254 upstream_branch = upstream_branch.replace('refs/heads/',
1255 'refs/remotes/%s/' % remote)
1256 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1257 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 self.upstream_branch = upstream_branch
1259 return self.upstream_branch
1260
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001262 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001263 remote, branch = None, self.GetBranch()
1264 seen_branches = set()
1265 while branch not in seen_branches:
1266 seen_branches.add(branch)
1267 remote, branch = self.FetchUpstreamTuple(branch)
1268 branch = ShortBranchName(branch)
1269 if remote != '.' or branch.startswith('refs/remotes'):
1270 break
1271 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001272 remotes = RunGit(['remote'], error_ok=True).split()
1273 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001274 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001275 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001276 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001277 logging.warn('Could not determine which remote this change is '
1278 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001279 else:
1280 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001281 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001282 branch = 'HEAD'
1283 if branch.startswith('refs/remotes'):
1284 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001285 elif branch.startswith('refs/branch-heads/'):
1286 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 else:
1288 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001289 return self._remote
1290
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001291 def GitSanityChecks(self, upstream_git_obj):
1292 """Checks git repo status and ensures diff is from local commits."""
1293
sbc@chromium.org79706062015-01-14 21:18:12 +00001294 if upstream_git_obj is None:
1295 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001296 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001297 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001298 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001299 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001300 return False
1301
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 # Verify the commit we're diffing against is in our current branch.
1303 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1304 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1305 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001306 print('ERROR: %s is not in the current branch. You may need to rebase '
1307 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 return False
1309
1310 # List the commits inside the diff, and verify they are all local.
1311 commits_in_diff = RunGit(
1312 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1313 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1314 remote_branch = remote_branch.strip()
1315 if code != 0:
1316 _, remote_branch = self.GetRemoteBranch()
1317
1318 commits_in_remote = RunGit(
1319 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1320
1321 common_commits = set(commits_in_diff) & set(commits_in_remote)
1322 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001323 print('ERROR: Your diff contains %d commits already in %s.\n'
1324 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1325 'the diff. If you are using a custom git flow, you can override'
1326 ' the reference used for this check with "git config '
1327 'gitcl.remotebranch <git-ref>".' % (
1328 len(common_commits), remote_branch, upstream_git_obj),
1329 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 return False
1331 return True
1332
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001333 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001334 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001335
1336 Returns None if it is not set.
1337 """
tandrii5d48c322016-08-18 16:19:37 -07001338 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 def GetRemoteUrl(self):
1341 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1342
1343 Returns None if there is no remote.
1344 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001345 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001346 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1347
1348 # If URL is pointing to a local directory, it is probably a git cache.
1349 if os.path.isdir(url):
1350 url = RunGit(['config', 'remote.%s.url' % remote],
1351 error_ok=True,
1352 cwd=url).strip()
1353 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001355 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001356 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001357 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001358 self.issue = self._GitGetBranchConfigValue(
1359 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001360 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 return self.issue
1362
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 def GetIssueURL(self):
1364 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001365 issue = self.GetIssue()
1366 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001367 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001368 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001370 def GetDescription(self, pretty=False, force=False):
1371 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001373 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 self.has_description = True
1375 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001376 # Set width to 72 columns + 2 space indent.
1377 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001379 lines = self.description.splitlines()
1380 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 return self.description
1382
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001383 def GetDescriptionFooters(self):
1384 """Returns (non_footer_lines, footers) for the commit message.
1385
1386 Returns:
1387 non_footer_lines (list(str)) - Simple list of description lines without
1388 any footer. The lines do not contain newlines, nor does the list contain
1389 the empty line between the message and the footers.
1390 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1391 [("Change-Id", "Ideadbeef...."), ...]
1392 """
1393 raw_description = self.GetDescription()
1394 msg_lines, _, footers = git_footers.split_footers(raw_description)
1395 if footers:
1396 msg_lines = msg_lines[:len(msg_lines)-1]
1397 return msg_lines, footers
1398
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001400 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001402 self.patchset = self._GitGetBranchConfigValue(
1403 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001404 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 return self.patchset
1406
1407 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001408 """Set this branch's patchset. If patchset=0, clears the patchset."""
1409 assert self.GetBranch()
1410 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001411 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001412 else:
1413 self.patchset = int(patchset)
1414 self._GitSetBranchConfigValue(
1415 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001417 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001418 """Set this branch's issue. If issue isn't given, clears the issue."""
1419 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001421 issue = int(issue)
1422 self._GitSetBranchConfigValue(
1423 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001424 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425 codereview_server = self._codereview_impl.GetCodereviewServer()
1426 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001427 self._GitSetBranchConfigValue(
1428 self._codereview_impl.CodereviewServerConfigKey(),
1429 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 else:
tandrii5d48c322016-08-18 16:19:37 -07001431 # Reset all of these just to be clean.
1432 reset_suffixes = [
1433 'last-upload-hash',
1434 self._codereview_impl.IssueConfigKey(),
1435 self._codereview_impl.PatchsetConfigKey(),
1436 self._codereview_impl.CodereviewServerConfigKey(),
1437 ] + self._PostUnsetIssueProperties()
1438 for prop in reset_suffixes:
1439 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001440 msg = RunGit(['log', '-1', '--format=%B']).strip()
1441 if msg and git_footers.get_footer_change_id(msg):
1442 print('WARNING: The change patched into this branch has a Change-Id. '
1443 'Removing it.')
1444 RunGit(['commit', '--amend', '-m',
1445 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001446 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001447 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448
dnjba1b0f32016-09-02 12:37:42 -07001449 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001450 if not self.GitSanityChecks(upstream_branch):
1451 DieWithError('\nGit sanity check failure')
1452
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001453 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001454 if not root:
1455 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001456 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001457
1458 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001459 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001460 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001461 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001462 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001463 except subprocess2.CalledProcessError:
1464 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001465 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001466 'This branch probably doesn\'t exist anymore. To reset the\n'
1467 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001468 ' git branch --set-upstream-to origin/master %s\n'
1469 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001470 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001471
maruel@chromium.org52424302012-08-29 15:14:30 +00001472 issue = self.GetIssue()
1473 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001474 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001475 description = self.GetDescription()
1476 else:
1477 # If the change was never uploaded, use the log messages of all commits
1478 # up to the branch point, as git cl upload will prefill the description
1479 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001480 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1481 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001482
1483 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001484 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001485 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001486 name,
1487 description,
1488 absroot,
1489 files,
1490 issue,
1491 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001492 author,
1493 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001494
dsansomee2d6fd92016-09-08 00:10:47 -07001495 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001496 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001497 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001498 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001499
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001500 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1501 """Sets the description for this CL remotely.
1502
1503 You can get description_lines and footers with GetDescriptionFooters.
1504
1505 Args:
1506 description_lines (list(str)) - List of CL description lines without
1507 newline characters.
1508 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1509 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1510 `List-Of-Tokens`). It will be case-normalized so that each token is
1511 title-cased.
1512 """
1513 new_description = '\n'.join(description_lines)
1514 if footers:
1515 new_description += '\n'
1516 for k, v in footers:
1517 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1518 if not git_footers.FOOTER_PATTERN.match(foot):
1519 raise ValueError('Invalid footer %r' % foot)
1520 new_description += foot + '\n'
1521 self.UpdateDescription(new_description, force)
1522
Edward Lesmes8e282792018-04-03 18:50:29 -04001523 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001524 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1525 try:
1526 return presubmit_support.DoPresubmitChecks(change, committing,
1527 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1528 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001529 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1530 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001531 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001532 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001533
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001534 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1535 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001536 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1537 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001538 else:
1539 # Assume url.
1540 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1541 urlparse.urlparse(issue_arg))
1542 if not parsed_issue_arg or not parsed_issue_arg.valid:
1543 DieWithError('Failed to parse issue argument "%s". '
1544 'Must be an issue number or a valid URL.' % issue_arg)
1545 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001546 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001547
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 def CMDUpload(self, options, git_diff_args, orig_args):
1549 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001550 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001552 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001553 else:
1554 if self.GetBranch() is None:
1555 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1556
1557 # Default to diffing against common ancestor of upstream branch
1558 base_branch = self.GetCommonAncestorWithUpstream()
1559 git_diff_args = [base_branch, 'HEAD']
1560
Aaron Gablec4c40d12017-05-22 11:49:53 -07001561 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1562 if not self.IsGerrit() and not self.GetIssue():
1563 print('=====================================')
1564 print('NOTICE: Rietveld is being deprecated. '
1565 'You can upload changes to Gerrit with')
1566 print(' git cl upload --gerrit')
1567 print('or set Gerrit to be your default code review tool with')
1568 print(' git config gerrit.host true')
1569 print('=====================================')
1570
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001571 # Fast best-effort checks to abort before running potentially
1572 # expensive hooks if uploading is likely to fail anyway. Passing these
1573 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001574 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001575 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576
1577 # Apply watchlists on upload.
1578 change = self.GetChange(base_branch, None)
1579 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1580 files = [f.LocalPath() for f in change.AffectedFiles()]
1581 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001582 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583
1584 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001585 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 # Set the reviewer list now so that presubmit checks can access it.
1587 change_description = ChangeDescription(change.FullDescriptionText())
1588 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001589 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001590 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001591 change)
1592 change.SetDescriptionText(change_description.description)
1593 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001594 may_prompt=not options.force,
1595 verbose=options.verbose,
1596 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001597 if not hook_results.should_continue():
1598 return 1
1599 if not options.reviewers and hook_results.reviewers:
1600 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001601 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001602
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001603 # TODO(tandrii): Checking local patchset against remote patchset is only
1604 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1605 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 latest_patchset = self.GetMostRecentPatchset()
1607 local_patchset = self.GetPatchset()
1608 if (latest_patchset and local_patchset and
1609 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001610 print('The last upload made from this repository was patchset #%d but '
1611 'the most recent patchset on the server is #%d.'
1612 % (local_patchset, latest_patchset))
1613 print('Uploading will still work, but if you\'ve uploaded to this '
1614 'issue from another machine or branch the patch you\'re '
1615 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001616 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617
Aaron Gable13101a62018-02-09 13:20:41 -08001618 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001619 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001621 if self.IsGerrit():
1622 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1623 options.cq_dry_run);
1624 else:
1625 if options.use_commit_queue:
1626 self.SetCQState(_CQState.COMMIT)
1627 elif options.cq_dry_run:
1628 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001629
tandrii5d48c322016-08-18 16:19:37 -07001630 _git_set_branch_config_value('last-upload-hash',
1631 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001632 # Run post upload hooks, if specified.
1633 if settings.GetRunPostUploadHook():
1634 presubmit_support.DoPostUploadExecuter(
1635 change,
1636 self,
1637 settings.GetRoot(),
1638 options.verbose,
1639 sys.stdout)
1640
1641 # Upload all dependencies if specified.
1642 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001643 print()
1644 print('--dependencies has been specified.')
1645 print('All dependent local branches will be re-uploaded.')
1646 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001647 # Remove the dependencies flag from args so that we do not end up in a
1648 # loop.
1649 orig_args.remove('--dependencies')
1650 ret = upload_branch_deps(self, orig_args)
1651 return ret
1652
Ravi Mistry31e7d562018-04-02 12:53:57 -04001653 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1654 """Sets labels on the change based on the provided flags.
1655
1656 Sets labels if issue is already uploaded and known, else returns without
1657 doing anything.
1658
1659 Args:
1660 enable_auto_submit: Sets Auto-Submit+1 on the change.
1661 use_commit_queue: Sets Commit-Queue+2 on the change.
1662 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1663 both use_commit_queue and cq_dry_run are true.
1664 """
1665 if not self.GetIssue():
1666 return
1667 try:
1668 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1669 cq_dry_run)
1670 return 0
1671 except KeyboardInterrupt:
1672 raise
1673 except:
1674 labels = []
1675 if enable_auto_submit:
1676 labels.append('Auto-Submit')
1677 if use_commit_queue or cq_dry_run:
1678 labels.append('Commit-Queue')
1679 print('WARNING: Failed to set label(s) on your change: %s\n'
1680 'Either:\n'
1681 ' * Your project does not have the above label(s),\n'
1682 ' * You don\'t have permission to set the above label(s),\n'
1683 ' * There\'s a bug in this code (see stack trace below).\n' %
1684 (', '.join(labels)))
1685 # Still raise exception so that stack trace is printed.
1686 raise
1687
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001688 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001689 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001690
1691 Issue must have been already uploaded and known.
1692 """
1693 assert new_state in _CQState.ALL_STATES
1694 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001695 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001696 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001697 return 0
1698 except KeyboardInterrupt:
1699 raise
1700 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001701 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001702 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001703 ' * Your project has no CQ,\n'
1704 ' * You don\'t have permission to change the CQ state,\n'
1705 ' * There\'s a bug in this code (see stack trace below).\n'
1706 'Consider specifying which bots to trigger manually or asking your '
1707 'project owners for permissions or contacting Chrome Infra at:\n'
1708 'https://www.chromium.org/infra\n\n' %
1709 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001710 # Still raise exception so that stack trace is printed.
1711 raise
1712
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 # Forward methods to codereview specific implementation.
1714
Aaron Gable636b13f2017-07-14 10:42:48 -07001715 def AddComment(self, message, publish=None):
1716 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001717
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001718 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001719 """Returns list of _CommentSummary for each comment.
1720
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001721 args:
1722 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001723 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001724 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001725
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001726 def CloseIssue(self):
1727 return self._codereview_impl.CloseIssue()
1728
1729 def GetStatus(self):
1730 return self._codereview_impl.GetStatus()
1731
1732 def GetCodereviewServer(self):
1733 return self._codereview_impl.GetCodereviewServer()
1734
tandriide281ae2016-10-12 06:02:30 -07001735 def GetIssueOwner(self):
1736 """Get owner from codereview, which may differ from this checkout."""
1737 return self._codereview_impl.GetIssueOwner()
1738
Edward Lemur707d70b2018-02-07 00:50:14 +01001739 def GetReviewers(self):
1740 return self._codereview_impl.GetReviewers()
1741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def GetMostRecentPatchset(self):
1743 return self._codereview_impl.GetMostRecentPatchset()
1744
tandriide281ae2016-10-12 06:02:30 -07001745 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001746 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001747 return self._codereview_impl.CannotTriggerTryJobReason()
1748
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001749 def GetTryJobProperties(self, patchset=None):
1750 """Returns dictionary of properties to launch try job."""
1751 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001752
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 def __getattr__(self, attr):
1754 # This is because lots of untested code accesses Rietveld-specific stuff
1755 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001756 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001757 # Note that child method defines __getattr__ as well, and forwards it here,
1758 # because _RietveldChangelistImpl is not cleaned up yet, and given
1759 # deprecation of Rietveld, it should probably be just removed.
1760 # Until that time, avoid infinite recursion by bypassing __getattr__
1761 # of implementation class.
1762 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763
1764
1765class _ChangelistCodereviewBase(object):
1766 """Abstract base class encapsulating codereview specifics of a changelist."""
1767 def __init__(self, changelist):
1768 self._changelist = changelist # instance of Changelist
1769
1770 def __getattr__(self, attr):
1771 # Forward methods to changelist.
1772 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1773 # _RietveldChangelistImpl to avoid this hack?
1774 return getattr(self._changelist, attr)
1775
1776 def GetStatus(self):
1777 """Apply a rough heuristic to give a simple summary of an issue's review
1778 or CQ status, assuming adherence to a common workflow.
1779
1780 Returns None if no issue for this branch, or specific string keywords.
1781 """
1782 raise NotImplementedError()
1783
1784 def GetCodereviewServer(self):
1785 """Returns server URL without end slash, like "https://codereview.com"."""
1786 raise NotImplementedError()
1787
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001788 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 """Fetches and returns description from the codereview server."""
1790 raise NotImplementedError()
1791
tandrii5d48c322016-08-18 16:19:37 -07001792 @classmethod
1793 def IssueConfigKey(cls):
1794 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001795 raise NotImplementedError()
1796
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001797 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001798 def PatchsetConfigKey(cls):
1799 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001800 raise NotImplementedError()
1801
tandrii5d48c322016-08-18 16:19:37 -07001802 @classmethod
1803 def CodereviewServerConfigKey(cls):
1804 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 raise NotImplementedError()
1806
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001807 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001808 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001809 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001810
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001811 def GetGerritObjForPresubmit(self):
1812 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1813 return None
1814
dsansomee2d6fd92016-09-08 00:10:47 -07001815 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001816 """Update the description on codereview site."""
1817 raise NotImplementedError()
1818
Aaron Gable636b13f2017-07-14 10:42:48 -07001819 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001820 """Posts a comment to the codereview site."""
1821 raise NotImplementedError()
1822
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001823 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001824 raise NotImplementedError()
1825
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001826 def CloseIssue(self):
1827 """Closes the issue."""
1828 raise NotImplementedError()
1829
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001830 def GetMostRecentPatchset(self):
1831 """Returns the most recent patchset number from the codereview site."""
1832 raise NotImplementedError()
1833
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001834 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001835 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001836 """Fetches and applies the issue.
1837
1838 Arguments:
1839 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1840 reject: if True, reject the failed patch instead of switching to 3-way
1841 merge. Rietveld only.
1842 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1843 only.
1844 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001845 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001846 """
1847 raise NotImplementedError()
1848
1849 @staticmethod
1850 def ParseIssueURL(parsed_url):
1851 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1852 failed."""
1853 raise NotImplementedError()
1854
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001855 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001856 """Best effort check that user is authenticated with codereview server.
1857
1858 Arguments:
1859 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001860 refresh: whether to attempt to refresh credentials. Ignored if not
1861 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001862 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001863 raise NotImplementedError()
1864
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001865 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001866 """Best effort check that uploading isn't supposed to fail for predictable
1867 reasons.
1868
1869 This method should raise informative exception if uploading shouldn't
1870 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001871
1872 Arguments:
1873 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001874 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001875 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001877 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001878 """Uploads a change to codereview."""
1879 raise NotImplementedError()
1880
Ravi Mistry31e7d562018-04-02 12:53:57 -04001881 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1882 """Sets labels on the change based on the provided flags.
1883
1884 Issue must have been already uploaded and known.
1885 """
1886 raise NotImplementedError()
1887
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001888 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001889 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001890
1891 Issue must have been already uploaded and known.
1892 """
1893 raise NotImplementedError()
1894
tandriie113dfd2016-10-11 10:20:12 -07001895 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001896 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001897 raise NotImplementedError()
1898
tandriide281ae2016-10-12 06:02:30 -07001899 def GetIssueOwner(self):
1900 raise NotImplementedError()
1901
Edward Lemur707d70b2018-02-07 00:50:14 +01001902 def GetReviewers(self):
1903 raise NotImplementedError()
1904
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001905 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001906 raise NotImplementedError()
1907
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908
1909class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001910
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001911 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001912 super(_RietveldChangelistImpl, self).__init__(changelist)
1913 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001914 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001915 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001917 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001918 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919 self._props = None
1920 self._rpc_server = None
1921
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001922 def GetCodereviewServer(self):
1923 if not self._rietveld_server:
1924 # If we're on a branch then get the server potentially associated
1925 # with that branch.
1926 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001927 self._rietveld_server = gclient_utils.UpgradeToHttps(
1928 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001929 if not self._rietveld_server:
1930 self._rietveld_server = settings.GetDefaultServerUrl()
1931 return self._rietveld_server
1932
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001933 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001934 """Best effort check that user is authenticated with Rietveld server."""
1935 if self._auth_config.use_oauth2:
1936 authenticator = auth.get_authenticator_for_host(
1937 self.GetCodereviewServer(), self._auth_config)
1938 if not authenticator.has_cached_credentials():
1939 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001940 if refresh:
1941 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001942
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001943 def EnsureCanUploadPatchset(self, force):
1944 # No checks for Rietveld because we are deprecating Rietveld.
1945 pass
1946
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001947 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001948 issue = self.GetIssue()
1949 assert issue
1950 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001951 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952 except urllib2.HTTPError as e:
1953 if e.code == 404:
1954 DieWithError(
1955 ('\nWhile fetching the description for issue %d, received a '
1956 '404 (not found)\n'
1957 'error. It is likely that you deleted this '
1958 'issue on the server. If this is the\n'
1959 'case, please run\n\n'
1960 ' git cl issue 0\n\n'
1961 'to clear the association with the deleted issue. Then run '
1962 'this command again.') % issue)
1963 else:
1964 DieWithError(
1965 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1966 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001967 print('Warning: Failed to retrieve CL description due to network '
1968 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001969 return ''
1970
1971 def GetMostRecentPatchset(self):
1972 return self.GetIssueProperties()['patchsets'][-1]
1973
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001974 def GetIssueProperties(self):
1975 if self._props is None:
1976 issue = self.GetIssue()
1977 if not issue:
1978 self._props = {}
1979 else:
1980 self._props = self.RpcServer().get_issue_properties(issue, True)
1981 return self._props
1982
tandriie113dfd2016-10-11 10:20:12 -07001983 def CannotTriggerTryJobReason(self):
1984 props = self.GetIssueProperties()
1985 if not props:
1986 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1987 if props.get('closed'):
1988 return 'CL %s is closed' % self.GetIssue()
1989 if props.get('private'):
1990 return 'CL %s is private' % self.GetIssue()
1991 return None
1992
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001993 def GetTryJobProperties(self, patchset=None):
1994 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001995 project = (self.GetIssueProperties() or {}).get('project')
1996 return {
1997 'issue': self.GetIssue(),
1998 'patch_project': project,
1999 'patch_storage': 'rietveld',
2000 'patchset': patchset or self.GetPatchset(),
2001 'rietveld': self.GetCodereviewServer(),
2002 }
2003
tandriide281ae2016-10-12 06:02:30 -07002004 def GetIssueOwner(self):
2005 return (self.GetIssueProperties() or {}).get('owner_email')
2006
Edward Lemur707d70b2018-02-07 00:50:14 +01002007 def GetReviewers(self):
2008 return (self.GetIssueProperties() or {}).get('reviewers')
2009
Aaron Gable636b13f2017-07-14 10:42:48 -07002010 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002011 return self.RpcServer().add_comment(self.GetIssue(), message)
2012
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002013 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002014 summary = []
2015 for message in self.GetIssueProperties().get('messages', []):
2016 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2017 summary.append(_CommentSummary(
2018 date=date,
2019 disapproval=bool(message['disapproval']),
2020 approval=bool(message['approval']),
2021 sender=message['sender'],
2022 message=message['text'],
2023 ))
2024 return summary
2025
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002026 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002027 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002028 or CQ status, assuming adherence to a common workflow.
2029
2030 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002031 * 'error' - error from review tool (including deleted issues)
2032 * 'unsent' - not sent for review
2033 * 'waiting' - waiting for review
2034 * 'reply' - waiting for owner to reply to review
2035 * 'not lgtm' - Code-Review label has been set negatively
2036 * 'lgtm' - LGTM from at least one approved reviewer
2037 * 'commit' - in the commit queue
2038 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002039 """
2040 if not self.GetIssue():
2041 return None
2042
2043 try:
2044 props = self.GetIssueProperties()
2045 except urllib2.HTTPError:
2046 return 'error'
2047
2048 if props.get('closed'):
2049 # Issue is closed.
2050 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002051 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002052 # Issue is in the commit queue.
2053 return 'commit'
2054
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002055 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002056 if not messages:
2057 # No message was sent.
2058 return 'unsent'
2059
2060 if get_approving_reviewers(props):
2061 return 'lgtm'
2062 elif get_approving_reviewers(props, disapproval=True):
2063 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002064
tandrii9d2c7a32016-06-22 03:42:45 -07002065 # Skip CQ messages that don't require owner's action.
2066 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2067 if 'Dry run:' in messages[-1]['text']:
2068 messages.pop()
2069 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2070 # This message always follows prior messages from CQ,
2071 # so skip this too.
2072 messages.pop()
2073 else:
2074 # This is probably a CQ messages warranting user attention.
2075 break
2076
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002077 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002078 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002079 return 'reply'
2080 return 'waiting'
2081
dsansomee2d6fd92016-09-08 00:10:47 -07002082 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002083 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002084
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002085 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002086 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002088 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002089 return self.SetFlags({flag: value})
2090
2091 def SetFlags(self, flags):
2092 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002093 """
phajdan.jr68598232016-08-10 03:28:28 -07002094 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002095 try:
tandrii4b233bd2016-07-06 03:50:29 -07002096 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002097 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002098 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002099 if e.code == 404:
2100 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2101 if e.code == 403:
2102 DieWithError(
2103 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002104 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002105 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002107 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108 """Returns an upload.RpcServer() to access this review's rietveld instance.
2109 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002110 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002111 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002112 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002113 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002114 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002116 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002117 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002118 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119
tandrii5d48c322016-08-18 16:19:37 -07002120 @classmethod
2121 def PatchsetConfigKey(cls):
2122 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123
tandrii5d48c322016-08-18 16:19:37 -07002124 @classmethod
2125 def CodereviewServerConfigKey(cls):
2126 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002127
Ravi Mistry31e7d562018-04-02 12:53:57 -04002128 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2129 raise NotImplementedError()
2130
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002131 def SetCQState(self, new_state):
2132 props = self.GetIssueProperties()
2133 if props.get('private'):
2134 DieWithError('Cannot set-commit on private issue')
2135
2136 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002137 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002138 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002139 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002140 else:
tandrii4b233bd2016-07-06 03:50:29 -07002141 assert new_state == _CQState.DRY_RUN
2142 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002143
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002144 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002145 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002146 # PatchIssue should never be called with a dirty tree. It is up to the
2147 # caller to check this, but just in case we assert here since the
2148 # consequences of the caller not checking this could be dire.
2149 assert(not git_common.is_dirty_git_tree('apply'))
2150 assert(parsed_issue_arg.valid)
2151 self._changelist.issue = parsed_issue_arg.issue
2152 if parsed_issue_arg.hostname:
2153 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2154
skobes6468b902016-10-24 08:45:10 -07002155 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2156 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2157 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 try:
skobes6468b902016-10-24 08:45:10 -07002159 scm_obj.apply_patch(patchset_object)
2160 except Exception as e:
2161 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002162 return 1
2163
2164 # If we had an issue, commit the current state and register the issue.
2165 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002166 self.SetIssue(self.GetIssue())
2167 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002168 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2169 'patch from issue %(i)s at patchset '
2170 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2171 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002172 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002174 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002175 return 0
2176
2177 @staticmethod
2178 def ParseIssueURL(parsed_url):
2179 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2180 return None
wychen3c1c1722016-08-04 11:46:36 -07002181 # Rietveld patch: https://domain/<number>/#ps<patchset>
2182 match = re.match(r'/(\d+)/$', parsed_url.path)
2183 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2184 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002185 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002186 issue=int(match.group(1)),
2187 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002188 hostname=parsed_url.netloc,
2189 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002190 # Typical url: https://domain/<issue_number>[/[other]]
2191 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2192 if match:
skobes6468b902016-10-24 08:45:10 -07002193 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002194 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002195 hostname=parsed_url.netloc,
2196 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002197 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2198 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2199 if match:
skobes6468b902016-10-24 08:45:10 -07002200 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002201 issue=int(match.group(1)),
2202 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002203 hostname=parsed_url.netloc,
2204 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002205 return None
2206
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002207 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002208 """Upload the patch to Rietveld."""
2209 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2210 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2212 if options.emulate_svn_auto_props:
2213 upload_args.append('--emulate_svn_auto_props')
2214
2215 change_desc = None
2216
2217 if options.email is not None:
2218 upload_args.extend(['--email', options.email])
2219
2220 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002221 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002222 upload_args.extend(['--title', options.title])
2223 if options.message:
2224 upload_args.extend(['--message', options.message])
2225 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002226 print('This branch is associated with issue %s. '
2227 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 else:
nodirca166002016-06-27 10:59:51 -07002229 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002230 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002231 if options.message:
2232 message = options.message
2233 else:
2234 message = CreateDescriptionFromLog(args)
2235 if options.title:
2236 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002237 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002238 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002239 change_desc.update_reviewers(options.reviewers, options.tbrs,
2240 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002242 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243
2244 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002245 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002246 return 1
2247
2248 upload_args.extend(['--message', change_desc.description])
2249 if change_desc.get_reviewers():
2250 upload_args.append('--reviewers=%s' % ','.join(
2251 change_desc.get_reviewers()))
2252 if options.send_mail:
2253 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002254 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002255 upload_args.append('--send_mail')
2256
2257 # We check this before applying rietveld.private assuming that in
2258 # rietveld.cc only addresses which we can send private CLs to are listed
2259 # if rietveld.private is set, and so we should ignore rietveld.cc only
2260 # when --private is specified explicitly on the command line.
2261 if options.private:
2262 logging.warn('rietveld.cc is ignored since private flag is specified. '
2263 'You need to review and add them manually if necessary.')
2264 cc = self.GetCCListWithoutDefault()
2265 else:
2266 cc = self.GetCCList()
2267 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002268 if change_desc.get_cced():
2269 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002270 if cc:
2271 upload_args.extend(['--cc', cc])
2272
2273 if options.private or settings.GetDefaultPrivateFlag() == "True":
2274 upload_args.append('--private')
2275
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002276 # Include the upstream repo's URL in the change -- this is useful for
2277 # projects that have their source spread across multiple repos.
2278 remote_url = self.GetGitBaseUrlFromConfig()
2279 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002280 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2281 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2282 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002285 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002286 if target_ref:
2287 upload_args.extend(['--target_ref', target_ref])
2288
2289 # Look for dependent patchsets. See crbug.com/480453 for more details.
2290 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2291 upstream_branch = ShortBranchName(upstream_branch)
2292 if remote is '.':
2293 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002294 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002296 print()
2297 print('Skipping dependency patchset upload because git config '
2298 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2299 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002300 else:
2301 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002302 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002303 auth_config=auth_config)
2304 branch_cl_issue_url = branch_cl.GetIssueURL()
2305 branch_cl_issue = branch_cl.GetIssue()
2306 branch_cl_patchset = branch_cl.GetPatchset()
2307 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2308 upload_args.extend(
2309 ['--depends_on_patchset', '%s:%s' % (
2310 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002311 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002312 '\n'
2313 'The current branch (%s) is tracking a local branch (%s) with '
2314 'an associated CL.\n'
2315 'Adding %s/#ps%s as a dependency patchset.\n'
2316 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2317 branch_cl_patchset))
2318
2319 project = settings.GetProject()
2320 if project:
2321 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002322 else:
2323 print()
2324 print('WARNING: Uploading without a project specified. Please ensure '
2325 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2326 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002327
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002328 try:
2329 upload_args = ['upload'] + upload_args + args
2330 logging.info('upload.RealMain(%s)', upload_args)
2331 issue, patchset = upload.RealMain(upload_args)
2332 issue = int(issue)
2333 patchset = int(patchset)
2334 except KeyboardInterrupt:
2335 sys.exit(1)
2336 except:
2337 # If we got an exception after the user typed a description for their
2338 # change, back up the description before re-raising.
2339 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002340 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002341 raise
2342
2343 if not self.GetIssue():
2344 self.SetIssue(issue)
2345 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002346 return 0
2347
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002348
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002349class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002350 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002351 # auth_config is Rietveld thing, kept here to preserve interface only.
2352 super(_GerritChangelistImpl, self).__init__(changelist)
2353 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002354 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002355 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002356 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002357 # Map from change number (issue) to its detail cache.
2358 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002359
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002360 if codereview_host is not None:
2361 assert not codereview_host.startswith('https://'), codereview_host
2362 self._gerrit_host = codereview_host
2363 self._gerrit_server = 'https://%s' % codereview_host
2364
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002365 def _GetGerritHost(self):
2366 # Lazy load of configs.
2367 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002368 if self._gerrit_host and '.' not in self._gerrit_host:
2369 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2370 # This happens for internal stuff http://crbug.com/614312.
2371 parsed = urlparse.urlparse(self.GetRemoteUrl())
2372 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002373 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002374 ' Your current remote is: %s' % self.GetRemoteUrl())
2375 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2376 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002377 return self._gerrit_host
2378
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002379 def _GetGitHost(self):
2380 """Returns git host to be used when uploading change to Gerrit."""
2381 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2382
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002383 def GetCodereviewServer(self):
2384 if not self._gerrit_server:
2385 # If we're on a branch then get the server potentially associated
2386 # with that branch.
2387 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002388 self._gerrit_server = self._GitGetBranchConfigValue(
2389 self.CodereviewServerConfigKey())
2390 if self._gerrit_server:
2391 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392 if not self._gerrit_server:
2393 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2394 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002395 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002396 parts[0] = parts[0] + '-review'
2397 self._gerrit_host = '.'.join(parts)
2398 self._gerrit_server = 'https://%s' % self._gerrit_host
2399 return self._gerrit_server
2400
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002401 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002402 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002403 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404
tandrii5d48c322016-08-18 16:19:37 -07002405 @classmethod
2406 def PatchsetConfigKey(cls):
2407 return 'gerritpatchset'
2408
2409 @classmethod
2410 def CodereviewServerConfigKey(cls):
2411 return 'gerritserver'
2412
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002413 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002414 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002415 if settings.GetGerritSkipEnsureAuthenticated():
2416 # For projects with unusual authentication schemes.
2417 # See http://crbug.com/603378.
2418 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002419 # Lazy-loader to identify Gerrit and Git hosts.
2420 if gerrit_util.GceAuthenticator.is_gce():
2421 return
2422 self.GetCodereviewServer()
2423 git_host = self._GetGitHost()
2424 assert self._gerrit_server and self._gerrit_host
2425 cookie_auth = gerrit_util.CookiesAuthenticator()
2426
2427 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2428 git_auth = cookie_auth.get_auth_header(git_host)
2429 if gerrit_auth and git_auth:
2430 if gerrit_auth == git_auth:
2431 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002432 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002433 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002434 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002435 ' %s\n'
2436 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002437 ' Consider running the following command:\n'
2438 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002439 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002440 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002441 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002442 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002443 cookie_auth.get_new_password_message(git_host)))
2444 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002445 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002446 return
2447 else:
2448 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002449 ([] if gerrit_auth else [self._gerrit_host]) +
2450 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002451 DieWithError('Credentials for the following hosts are required:\n'
2452 ' %s\n'
2453 'These are read from %s (or legacy %s)\n'
2454 '%s' % (
2455 '\n '.join(missing),
2456 cookie_auth.get_gitcookies_path(),
2457 cookie_auth.get_netrc_path(),
2458 cookie_auth.get_new_password_message(git_host)))
2459
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002460 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002461 if not self.GetIssue():
2462 return
2463
2464 # Warm change details cache now to avoid RPCs later, reducing latency for
2465 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002466 self._GetChangeDetail(
2467 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002468
2469 status = self._GetChangeDetail()['status']
2470 if status in ('MERGED', 'ABANDONED'):
2471 DieWithError('Change %s has been %s, new uploads are not allowed' %
2472 (self.GetIssueURL(),
2473 'submitted' if status == 'MERGED' else 'abandoned'))
2474
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002475 if gerrit_util.GceAuthenticator.is_gce():
2476 return
2477 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2478 self._GetGerritHost())
2479 if self.GetIssueOwner() == cookies_user:
2480 return
2481 logging.debug('change %s owner is %s, cookies user is %s',
2482 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002483 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002484 # so ask what Gerrit thinks of this user.
2485 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2486 if details['email'] == self.GetIssueOwner():
2487 return
2488 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002489 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002490 'as %s.\n'
2491 'Uploading may fail due to lack of permissions.' %
2492 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2493 confirm_or_exit(action='upload')
2494
2495
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002496 def _PostUnsetIssueProperties(self):
2497 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002498 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002499
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002500 def GetGerritObjForPresubmit(self):
2501 return presubmit_support.GerritAccessor(self._GetGerritHost())
2502
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002503 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002504 """Apply a rough heuristic to give a simple summary of an issue's review
2505 or CQ status, assuming adherence to a common workflow.
2506
2507 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002508 * 'error' - error from review tool (including deleted issues)
2509 * 'unsent' - no reviewers added
2510 * 'waiting' - waiting for review
2511 * 'reply' - waiting for uploader to reply to review
2512 * 'lgtm' - Code-Review label has been set
2513 * 'commit' - in the commit queue
2514 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002515 """
2516 if not self.GetIssue():
2517 return None
2518
2519 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002520 data = self._GetChangeDetail([
2521 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002522 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002523 return 'error'
2524
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002525 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002526 return 'closed'
2527
Aaron Gable9ab38c62017-04-06 14:36:33 -07002528 if data['labels'].get('Commit-Queue', {}).get('approved'):
2529 # The section will have an "approved" subsection if anyone has voted
2530 # the maximum value on the label.
2531 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002532
Aaron Gable9ab38c62017-04-06 14:36:33 -07002533 if data['labels'].get('Code-Review', {}).get('approved'):
2534 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002535
2536 if not data.get('reviewers', {}).get('REVIEWER', []):
2537 return 'unsent'
2538
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002539 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002540 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2541 last_message_author = messages.pop().get('author', {})
2542 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002543 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2544 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002545 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002546 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002547 if last_message_author.get('_account_id') == owner:
2548 # Most recent message was by owner.
2549 return 'waiting'
2550 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002551 # Some reply from non-owner.
2552 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002553
2554 # Somehow there are no messages even though there are reviewers.
2555 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002556
2557 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002558 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002559 patchset = data['revisions'][data['current_revision']]['_number']
2560 self.SetPatchset(patchset)
2561 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002562
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002563 def FetchDescription(self, force=False):
2564 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2565 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002566 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002567 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002568
dsansomee2d6fd92016-09-08 00:10:47 -07002569 def UpdateDescriptionRemote(self, description, force=False):
2570 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2571 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002572 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002573 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002574 'unpublished edit. Either publish the edit in the Gerrit web UI '
2575 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002576
2577 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2578 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002579 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002580 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002581
Aaron Gable636b13f2017-07-14 10:42:48 -07002582 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002583 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002584 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002585
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002586 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002587 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002588 messages = self._GetChangeDetail(
2589 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2590 file_comments = gerrit_util.GetChangeComments(
2591 self._GetGerritHost(), self.GetIssue())
2592
2593 # Build dictionary of file comments for easy access and sorting later.
2594 # {author+date: {path: {patchset: {line: url+message}}}}
2595 comments = collections.defaultdict(
2596 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2597 for path, line_comments in file_comments.iteritems():
2598 for comment in line_comments:
2599 if comment.get('tag', '').startswith('autogenerated'):
2600 continue
2601 key = (comment['author']['email'], comment['updated'])
2602 if comment.get('side', 'REVISION') == 'PARENT':
2603 patchset = 'Base'
2604 else:
2605 patchset = 'PS%d' % comment['patch_set']
2606 line = comment.get('line', 0)
2607 url = ('https://%s/c/%s/%s/%s#%s%s' %
2608 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2609 'b' if comment.get('side') == 'PARENT' else '',
2610 str(line) if line else ''))
2611 comments[key][path][patchset][line] = (url, comment['message'])
2612
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002613 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002614 for msg in messages:
2615 # Don't bother showing autogenerated messages.
2616 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2617 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002618 # Gerrit spits out nanoseconds.
2619 assert len(msg['date'].split('.')[-1]) == 9
2620 date = datetime.datetime.strptime(msg['date'][:-3],
2621 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002622 message = msg['message']
2623 key = (msg['author']['email'], msg['date'])
2624 if key in comments:
2625 message += '\n'
2626 for path, patchsets in sorted(comments.get(key, {}).items()):
2627 if readable:
2628 message += '\n%s' % path
2629 for patchset, lines in sorted(patchsets.items()):
2630 for line, (url, content) in sorted(lines.items()):
2631 if line:
2632 line_str = 'Line %d' % line
2633 path_str = '%s:%d:' % (path, line)
2634 else:
2635 line_str = 'File comment'
2636 path_str = '%s:0:' % path
2637 if readable:
2638 message += '\n %s, %s: %s' % (patchset, line_str, url)
2639 message += '\n %s\n' % content
2640 else:
2641 message += '\n%s ' % path_str
2642 message += '\n%s\n' % content
2643
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002644 summary.append(_CommentSummary(
2645 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002646 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002647 sender=msg['author']['email'],
2648 # These could be inferred from the text messages and correlated with
2649 # Code-Review label maximum, however this is not reliable.
2650 # Leaving as is until the need arises.
2651 approval=False,
2652 disapproval=False,
2653 ))
2654 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002655
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002656 def CloseIssue(self):
2657 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2658
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002659 def SubmitIssue(self, wait_for_merge=True):
2660 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2661 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002662
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002663 def _GetChangeDetail(self, options=None, issue=None,
2664 no_cache=False):
2665 """Returns details of the issue by querying Gerrit and caching results.
2666
2667 If fresh data is needed, set no_cache=True which will clear cache and
2668 thus new data will be fetched from Gerrit.
2669 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002670 options = options or []
2671 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002672 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002673
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002674 # Optimization to avoid multiple RPCs:
2675 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2676 'CURRENT_COMMIT' not in options):
2677 options.append('CURRENT_COMMIT')
2678
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002679 # Normalize issue and options for consistent keys in cache.
2680 issue = str(issue)
2681 options = [o.upper() for o in options]
2682
2683 # Check in cache first unless no_cache is True.
2684 if no_cache:
2685 self._detail_cache.pop(issue, None)
2686 else:
2687 options_set = frozenset(options)
2688 for cached_options_set, data in self._detail_cache.get(issue, []):
2689 # Assumption: data fetched before with extra options is suitable
2690 # for return for a smaller set of options.
2691 # For example, if we cached data for
2692 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2693 # and request is for options=[CURRENT_REVISION],
2694 # THEN we can return prior cached data.
2695 if options_set.issubset(cached_options_set):
2696 return data
2697
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002698 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002699 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002700 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002701 except gerrit_util.GerritError as e:
2702 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002703 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002704 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002705
2706 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002707 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002708
agable32978d92016-11-01 12:55:02 -07002709 def _GetChangeCommit(self, issue=None):
2710 issue = issue or self.GetIssue()
2711 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002712 try:
2713 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2714 except gerrit_util.GerritError as e:
2715 if e.http_status == 404:
2716 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2717 raise
agable32978d92016-11-01 12:55:02 -07002718 return data
2719
Olivier Robin75ee7252018-04-13 10:02:56 +02002720 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002721 if git_common.is_dirty_git_tree('land'):
2722 return 1
tandriid60367b2016-06-22 05:25:12 -07002723 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2724 if u'Commit-Queue' in detail.get('labels', {}):
2725 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002726 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2727 'which can test and land changes for you. '
2728 'Are you sure you wish to bypass it?\n',
2729 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002730
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002731 differs = True
tandriic4344b52016-08-29 06:04:54 -07002732 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002733 # Note: git diff outputs nothing if there is no diff.
2734 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002735 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002736 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002737 if detail['current_revision'] == last_upload:
2738 differs = False
2739 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002740 print('WARNING: Local branch contents differ from latest uploaded '
2741 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002742 if differs:
2743 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002744 confirm_or_exit(
2745 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2746 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002747 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002748 elif not bypass_hooks:
2749 hook_results = self.RunHook(
2750 committing=True,
2751 may_prompt=not force,
2752 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002753 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2754 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002755 if not hook_results.should_continue():
2756 return 1
2757
2758 self.SubmitIssue(wait_for_merge=True)
2759 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002760 links = self._GetChangeCommit().get('web_links', [])
2761 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002762 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002763 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002764 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002765 return 0
2766
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002767 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002768 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002769 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002770 assert not directory
2771 assert parsed_issue_arg.valid
2772
2773 self._changelist.issue = parsed_issue_arg.issue
2774
2775 if parsed_issue_arg.hostname:
2776 self._gerrit_host = parsed_issue_arg.hostname
2777 self._gerrit_server = 'https://%s' % self._gerrit_host
2778
tandriic2405f52016-10-10 08:13:15 -07002779 try:
2780 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002781 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002782 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002783
2784 if not parsed_issue_arg.patchset:
2785 # Use current revision by default.
2786 revision_info = detail['revisions'][detail['current_revision']]
2787 patchset = int(revision_info['_number'])
2788 else:
2789 patchset = parsed_issue_arg.patchset
2790 for revision_info in detail['revisions'].itervalues():
2791 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2792 break
2793 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002794 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002795 (parsed_issue_arg.patchset, self.GetIssue()))
2796
Aaron Gable697a91b2018-01-19 15:20:15 -08002797 remote_url = self._changelist.GetRemoteUrl()
2798 if remote_url.endswith('.git'):
2799 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002800 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002801
2802 if remote_url != fetch_info['url']:
2803 DieWithError('Trying to patch a change from %s but this repo appears '
2804 'to be %s.' % (fetch_info['url'], remote_url))
2805
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002806 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002807
Aaron Gable62619a32017-06-16 08:22:09 -07002808 if force:
2809 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2810 print('Checked out commit for change %i patchset %i locally' %
2811 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002812 elif nocommit:
2813 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2814 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002815 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002816 RunGit(['cherry-pick', 'FETCH_HEAD'])
2817 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002818 (parsed_issue_arg.issue, patchset))
2819 print('Note: this created a local commit which does not have '
2820 'the same hash as the one uploaded for review. This will make '
2821 'uploading changes based on top of this branch difficult.\n'
2822 'If you want to do that, use "git cl patch --force" instead.')
2823
Stefan Zagerd08043c2017-10-12 12:07:02 -07002824 if self.GetBranch():
2825 self.SetIssue(parsed_issue_arg.issue)
2826 self.SetPatchset(patchset)
2827 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2828 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2829 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2830 else:
2831 print('WARNING: You are in detached HEAD state.\n'
2832 'The patch has been applied to your checkout, but you will not be '
2833 'able to upload a new patch set to the gerrit issue.\n'
2834 'Try using the \'-b\' option if you would like to work on a '
2835 'branch and/or upload a new patch set.')
2836
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002837 return 0
2838
2839 @staticmethod
2840 def ParseIssueURL(parsed_url):
2841 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2842 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002843 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2844 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002845 # Short urls like https://domain/<issue_number> can be used, but don't allow
2846 # specifying the patchset (you'd 404), but we allow that here.
2847 if parsed_url.path == '/':
2848 part = parsed_url.fragment
2849 else:
2850 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002851 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002852 if match:
2853 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002854 issue=int(match.group(3)),
2855 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002856 hostname=parsed_url.netloc,
2857 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002858 return None
2859
tandrii16e0b4e2016-06-07 10:34:28 -07002860 def _GerritCommitMsgHookCheck(self, offer_removal):
2861 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2862 if not os.path.exists(hook):
2863 return
2864 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2865 # custom developer made one.
2866 data = gclient_utils.FileRead(hook)
2867 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2868 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002869 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002870 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002871 'and may interfere with it in subtle ways.\n'
2872 'We recommend you remove the commit-msg hook.')
2873 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002874 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002875 gclient_utils.rm_file_or_tree(hook)
2876 print('Gerrit commit-msg hook removed.')
2877 else:
2878 print('OK, will keep Gerrit commit-msg hook in place.')
2879
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002880 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002881 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002882 if options.squash and options.no_squash:
2883 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002884
2885 if not options.squash and not options.no_squash:
2886 # Load default for user, repo, squash=true, in this order.
2887 options.squash = settings.GetSquashGerritUploads()
2888 elif options.no_squash:
2889 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002890
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002891 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002892 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002893
Aaron Gableb56ad332017-01-06 15:24:31 -08002894 # This may be None; default fallback value is determined in logic below.
2895 title = options.title
2896
Dominic Battre7d1c4842017-10-27 09:17:28 +02002897 # Extract bug number from branch name.
2898 bug = options.bug
2899 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2900 if not bug and match:
2901 bug = match.group(1)
2902
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002903 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002904 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002905 if self.GetIssue():
2906 # Try to get the message from a previous upload.
2907 message = self.GetDescription()
2908 if not message:
2909 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002910 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002911 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002912 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002913 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002914 # When uploading a subsequent patchset, -m|--message is taken
2915 # as the patchset title if --title was not provided.
2916 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002917 else:
2918 default_title = RunGit(
2919 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002920 if options.force:
2921 title = default_title
2922 else:
2923 title = ask_for_data(
2924 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002925 change_id = self._GetChangeDetail()['change_id']
2926 while True:
2927 footer_change_ids = git_footers.get_footer_change_id(message)
2928 if footer_change_ids == [change_id]:
2929 break
2930 if not footer_change_ids:
2931 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002932 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002933 continue
2934 # There is already a valid footer but with different or several ids.
2935 # Doing this automatically is non-trivial as we don't want to lose
2936 # existing other footers, yet we want to append just 1 desired
2937 # Change-Id. Thus, just create a new footer, but let user verify the
2938 # new description.
2939 message = '%s\n\nChange-Id: %s' % (message, change_id)
2940 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002941 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002942 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002943 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002944 'Please, check the proposed correction to the description, '
2945 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2946 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2947 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002948 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002949 if not options.force:
2950 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002951 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002952 message = change_desc.description
2953 if not message:
2954 DieWithError("Description is empty. Aborting...")
2955 # Continue the while loop.
2956 # Sanity check of this code - we should end up with proper message
2957 # footer.
2958 assert [change_id] == git_footers.get_footer_change_id(message)
2959 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002960 else: # if not self.GetIssue()
2961 if options.message:
2962 message = options.message
2963 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002964 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002965 if options.title:
2966 message = options.title + '\n\n' + message
2967 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002968
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002969 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002970 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002971 # On first upload, patchset title is always this string, while
2972 # --title flag gets converted to first line of message.
2973 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002974 if not change_desc.description:
2975 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002976 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002977 if len(change_ids) > 1:
2978 DieWithError('too many Change-Id footers, at most 1 allowed.')
2979 if not change_ids:
2980 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002981 change_desc.set_description(git_footers.add_footer_change_id(
2982 change_desc.description,
2983 GenerateGerritChangeId(change_desc.description)))
2984 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002985 assert len(change_ids) == 1
2986 change_id = change_ids[0]
2987
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002988 if options.reviewers or options.tbrs or options.add_owners_to:
2989 change_desc.update_reviewers(options.reviewers, options.tbrs,
2990 options.add_owners_to, change)
2991
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002992 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002993 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2994 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002995 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002996 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2997 desc_tempfile.write(change_desc.description)
2998 desc_tempfile.close()
2999 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3000 '-F', desc_tempfile.name]).strip()
3001 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003002 else:
3003 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003004 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003005 if not change_desc.description:
3006 DieWithError("Description is empty. Aborting...")
3007
3008 if not git_footers.get_footer_change_id(change_desc.description):
3009 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003010 change_desc.set_description(
3011 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003012 if options.reviewers or options.tbrs or options.add_owners_to:
3013 change_desc.update_reviewers(options.reviewers, options.tbrs,
3014 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003016 # For no-squash mode, we assume the remote called "origin" is the one we
3017 # want. It is not worthwhile to support different workflows for
3018 # no-squash mode.
3019 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003020 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3021
3022 assert change_desc
3023 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3024 ref_to_push)]).splitlines()
3025 if len(commits) > 1:
3026 print('WARNING: This will upload %d commits. Run the following command '
3027 'to see which commits will be uploaded: ' % len(commits))
3028 print('git log %s..%s' % (parent, ref_to_push))
3029 print('You can also use `git squash-branch` to squash these into a '
3030 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003031 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003032
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003033 if options.reviewers or options.tbrs or options.add_owners_to:
3034 change_desc.update_reviewers(options.reviewers, options.tbrs,
3035 options.add_owners_to, change)
3036
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003037 # Extra options that can be specified at push time. Doc:
3038 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003039 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003040
Aaron Gable844cf292017-06-28 11:32:59 -07003041 # By default, new changes are started in WIP mode, and subsequent patchsets
3042 # don't send email. At any time, passing --send-mail will mark the change
3043 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003044 if options.send_mail:
3045 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003046 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04003047 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003048 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003049 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003050 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003051
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003052 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003053 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003054
Aaron Gable9b713dd2016-12-14 16:04:21 -08003055 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003056 # Punctuation and whitespace in |title| must be percent-encoded.
3057 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003058
agablec6787972016-09-09 16:13:34 -07003059 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003060 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003061
rmistry9eadede2016-09-19 11:22:43 -07003062 if options.topic:
3063 # Documentation on Gerrit topics is here:
3064 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003065 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003066
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003067 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003068 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003069 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003070 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003071 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3072
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003073 refspec_suffix = ''
3074 if refspec_opts:
3075 refspec_suffix = '%' + ','.join(refspec_opts)
3076 assert ' ' not in refspec_suffix, (
3077 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3078 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3079
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003080 try:
3081 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003082 ['git', 'push', self.GetRemoteUrl(), refspec],
3083 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003084 # Flush after every line: useful for seeing progress when running as
3085 # recipe.
3086 filter_fn=lambda _: sys.stdout.flush())
3087 except subprocess2.CalledProcessError:
3088 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003089 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003090 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003091 'credential problems:\n'
3092 ' git cl creds-check\n',
3093 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003094
3095 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003096 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003097 change_numbers = [m.group(1)
3098 for m in map(regex.match, push_stdout.splitlines())
3099 if m]
3100 if len(change_numbers) != 1:
3101 DieWithError(
3102 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003103 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003104 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003105 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003106
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003107 reviewers = sorted(change_desc.get_reviewers())
3108
tandrii88189772016-09-29 04:29:57 -07003109 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003110 if not options.private:
3111 cc = self.GetCCList().split(',')
3112 else:
3113 cc = []
tandrii88189772016-09-29 04:29:57 -07003114 if options.cc:
3115 cc.extend(options.cc)
3116 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003117 if change_desc.get_cced():
3118 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003119
3120 gerrit_util.AddReviewers(
3121 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3122 notify=bool(options.send_mail))
3123
Aaron Gablefd238082017-06-07 13:42:34 -07003124 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003125 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3126 score = 1
3127 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3128 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3129 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003130 gerrit_util.SetReview(
3131 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003132 msg='Self-approving for TBR',
3133 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003134
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003135 return 0
3136
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003137 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3138 change_desc):
3139 """Computes parent of the generated commit to be uploaded to Gerrit.
3140
3141 Returns revision or a ref name.
3142 """
3143 if custom_cl_base:
3144 # Try to avoid creating additional unintended CLs when uploading, unless
3145 # user wants to take this risk.
3146 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3147 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3148 local_ref_of_target_remote])
3149 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003150 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003151 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3152 'If you proceed with upload, more than 1 CL may be created by '
3153 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3154 'If you are certain that specified base `%s` has already been '
3155 'uploaded to Gerrit as another CL, you may proceed.\n' %
3156 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3157 if not force:
3158 confirm_or_exit(
3159 'Do you take responsibility for cleaning up potential mess '
3160 'resulting from proceeding with upload?',
3161 action='upload')
3162 return custom_cl_base
3163
Aaron Gablef97e33d2017-03-30 15:44:27 -07003164 if remote != '.':
3165 return self.GetCommonAncestorWithUpstream()
3166
3167 # If our upstream branch is local, we base our squashed commit on its
3168 # squashed version.
3169 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3170
Aaron Gablef97e33d2017-03-30 15:44:27 -07003171 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003172 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003173
3174 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003175 # TODO(tandrii): consider checking parent change in Gerrit and using its
3176 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3177 # the tree hash of the parent branch. The upside is less likely bogus
3178 # requests to reupload parent change just because it's uploadhash is
3179 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003180 parent = RunGit(['config',
3181 'branch.%s.gerritsquashhash' % upstream_branch_name],
3182 error_ok=True).strip()
3183 # Verify that the upstream branch has been uploaded too, otherwise
3184 # Gerrit will create additional CLs when uploading.
3185 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3186 RunGitSilent(['rev-parse', parent + ':'])):
3187 DieWithError(
3188 '\nUpload upstream branch %s first.\n'
3189 'It is likely that this branch has been rebased since its last '
3190 'upload, so you just need to upload it again.\n'
3191 '(If you uploaded it with --no-squash, then branch dependencies '
3192 'are not supported, and you should reupload with --squash.)'
3193 % upstream_branch_name,
3194 change_desc)
3195 return parent
3196
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003197 def _AddChangeIdToCommitMessage(self, options, args):
3198 """Re-commits using the current message, assumes the commit hook is in
3199 place.
3200 """
3201 log_desc = options.message or CreateDescriptionFromLog(args)
3202 git_command = ['commit', '--amend', '-m', log_desc]
3203 RunGit(git_command)
3204 new_log_desc = CreateDescriptionFromLog(args)
3205 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003206 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003207 return new_log_desc
3208 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003209 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003210
Ravi Mistry31e7d562018-04-02 12:53:57 -04003211 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3212 """Sets labels on the change based on the provided flags."""
3213 labels = {}
3214 notify = None;
3215 if enable_auto_submit:
3216 labels['Auto-Submit'] = 1
3217 if use_commit_queue:
3218 labels['Commit-Queue'] = 2
3219 elif cq_dry_run:
3220 labels['Commit-Queue'] = 1
3221 notify = False
3222 if labels:
3223 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3224 labels=labels, notify=notify)
3225
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003226 def SetCQState(self, new_state):
3227 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003228 vote_map = {
3229 _CQState.NONE: 0,
3230 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003231 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003232 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003233 labels = {'Commit-Queue': vote_map[new_state]}
3234 notify = False if new_state == _CQState.DRY_RUN else None
3235 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3236 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003237
tandriie113dfd2016-10-11 10:20:12 -07003238 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003239 try:
3240 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003241 except GerritChangeNotExists:
3242 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003243
3244 if data['status'] in ('ABANDONED', 'MERGED'):
3245 return 'CL %s is closed' % self.GetIssue()
3246
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003247 def GetTryJobProperties(self, patchset=None):
3248 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003249 data = self._GetChangeDetail(['ALL_REVISIONS'])
3250 patchset = int(patchset or self.GetPatchset())
3251 assert patchset
3252 revision_data = None # Pylint wants it to be defined.
3253 for revision_data in data['revisions'].itervalues():
3254 if int(revision_data['_number']) == patchset:
3255 break
3256 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003257 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003258 (patchset, self.GetIssue()))
3259 return {
3260 'patch_issue': self.GetIssue(),
3261 'patch_set': patchset or self.GetPatchset(),
3262 'patch_project': data['project'],
3263 'patch_storage': 'gerrit',
3264 'patch_ref': revision_data['fetch']['http']['ref'],
3265 'patch_repository_url': revision_data['fetch']['http']['url'],
3266 'patch_gerrit_url': self.GetCodereviewServer(),
3267 }
tandriie113dfd2016-10-11 10:20:12 -07003268
tandriide281ae2016-10-12 06:02:30 -07003269 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003270 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003271
Edward Lemur707d70b2018-02-07 00:50:14 +01003272 def GetReviewers(self):
3273 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3274 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3275
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003276
3277_CODEREVIEW_IMPLEMENTATIONS = {
3278 'rietveld': _RietveldChangelistImpl,
3279 'gerrit': _GerritChangelistImpl,
3280}
3281
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003282
iannuccie53c9352016-08-17 14:40:40 -07003283def _add_codereview_issue_select_options(parser, extra=""):
3284 _add_codereview_select_options(parser)
3285
3286 text = ('Operate on this issue number instead of the current branch\'s '
3287 'implicit issue.')
3288 if extra:
3289 text += ' '+extra
3290 parser.add_option('-i', '--issue', type=int, help=text)
3291
3292
3293def _process_codereview_issue_select_options(parser, options):
3294 _process_codereview_select_options(parser, options)
3295 if options.issue is not None and not options.forced_codereview:
3296 parser.error('--issue must be specified with either --rietveld or --gerrit')
3297
3298
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003299def _add_codereview_select_options(parser):
3300 """Appends --gerrit and --rietveld options to force specific codereview."""
3301 parser.codereview_group = optparse.OptionGroup(
3302 parser, 'EXPERIMENTAL! Codereview override options')
3303 parser.add_option_group(parser.codereview_group)
3304 parser.codereview_group.add_option(
3305 '--gerrit', action='store_true',
3306 help='Force the use of Gerrit for codereview')
3307 parser.codereview_group.add_option(
3308 '--rietveld', action='store_true',
3309 help='Force the use of Rietveld for codereview')
3310
3311
3312def _process_codereview_select_options(parser, options):
3313 if options.gerrit and options.rietveld:
3314 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3315 options.forced_codereview = None
3316 if options.gerrit:
3317 options.forced_codereview = 'gerrit'
3318 elif options.rietveld:
3319 options.forced_codereview = 'rietveld'
3320
3321
tandriif9aefb72016-07-01 09:06:51 -07003322def _get_bug_line_values(default_project, bugs):
3323 """Given default_project and comma separated list of bugs, yields bug line
3324 values.
3325
3326 Each bug can be either:
3327 * a number, which is combined with default_project
3328 * string, which is left as is.
3329
3330 This function may produce more than one line, because bugdroid expects one
3331 project per line.
3332
3333 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3334 ['v8:123', 'chromium:789']
3335 """
3336 default_bugs = []
3337 others = []
3338 for bug in bugs.split(','):
3339 bug = bug.strip()
3340 if bug:
3341 try:
3342 default_bugs.append(int(bug))
3343 except ValueError:
3344 others.append(bug)
3345
3346 if default_bugs:
3347 default_bugs = ','.join(map(str, default_bugs))
3348 if default_project:
3349 yield '%s:%s' % (default_project, default_bugs)
3350 else:
3351 yield default_bugs
3352 for other in sorted(others):
3353 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3354 yield other
3355
3356
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003357class ChangeDescription(object):
3358 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003359 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003360 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003361 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003362 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003363 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3364 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3365 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3366 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003367
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003368 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003369 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003370
agable@chromium.org42c20792013-09-12 17:34:49 +00003371 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003372 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003373 return '\n'.join(self._description_lines)
3374
3375 def set_description(self, desc):
3376 if isinstance(desc, basestring):
3377 lines = desc.splitlines()
3378 else:
3379 lines = [line.rstrip() for line in desc]
3380 while lines and not lines[0]:
3381 lines.pop(0)
3382 while lines and not lines[-1]:
3383 lines.pop(-1)
3384 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003385
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003386 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3387 """Rewrites the R=/TBR= line(s) as a single line each.
3388
3389 Args:
3390 reviewers (list(str)) - list of additional emails to use for reviewers.
3391 tbrs (list(str)) - list of additional emails to use for TBRs.
3392 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3393 the change that are missing OWNER coverage. If this is not None, you
3394 must also pass a value for `change`.
3395 change (Change) - The Change that should be used for OWNERS lookups.
3396 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003397 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003398 assert isinstance(tbrs, list), tbrs
3399
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003400 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003401 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003402
3403 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003404 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003405
3406 reviewers = set(reviewers)
3407 tbrs = set(tbrs)
3408 LOOKUP = {
3409 'TBR': tbrs,
3410 'R': reviewers,
3411 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003412
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003413 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003414 regexp = re.compile(self.R_LINE)
3415 matches = [regexp.match(line) for line in self._description_lines]
3416 new_desc = [l for i, l in enumerate(self._description_lines)
3417 if not matches[i]]
3418 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003419
agable@chromium.org42c20792013-09-12 17:34:49 +00003420 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003421
3422 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003423 for match in matches:
3424 if not match:
3425 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003426 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3427
3428 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003429 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003430 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003431 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003432 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003433 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003434 LOOKUP[add_owners_to].update(
3435 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003436
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003437 # If any folks ended up in both groups, remove them from tbrs.
3438 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003439
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003440 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3441 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003442
3443 # Put the new lines in the description where the old first R= line was.
3444 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3445 if 0 <= line_loc < len(self._description_lines):
3446 if new_tbr_line:
3447 self._description_lines.insert(line_loc, new_tbr_line)
3448 if new_r_line:
3449 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003450 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003451 if new_r_line:
3452 self.append_footer(new_r_line)
3453 if new_tbr_line:
3454 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003455
Aaron Gable3a16ed12017-03-23 10:51:55 -07003456 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003457 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003458 self.set_description([
3459 '# Enter a description of the change.',
3460 '# This will be displayed on the codereview site.',
3461 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003462 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003463 '--------------------',
3464 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003465
agable@chromium.org42c20792013-09-12 17:34:49 +00003466 regexp = re.compile(self.BUG_LINE)
3467 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003468 prefix = settings.GetBugPrefix()
3469 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003470 if git_footer:
3471 self.append_footer('Bug: %s' % ', '.join(values))
3472 else:
3473 for value in values:
3474 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003475
agable@chromium.org42c20792013-09-12 17:34:49 +00003476 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003477 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003478 if not content:
3479 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003480 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003481
Bruce Dawson2377b012018-01-11 16:46:49 -08003482 # Strip off comments and default inserted "Bug:" line.
3483 clean_lines = [line.rstrip() for line in lines if not
3484 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003485 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003486 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003487 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003488
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003489 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003490 """Adds a footer line to the description.
3491
3492 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3493 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3494 that Gerrit footers are always at the end.
3495 """
3496 parsed_footer_line = git_footers.parse_footer(line)
3497 if parsed_footer_line:
3498 # Line is a gerrit footer in the form: Footer-Key: any value.
3499 # Thus, must be appended observing Gerrit footer rules.
3500 self.set_description(
3501 git_footers.add_footer(self.description,
3502 key=parsed_footer_line[0],
3503 value=parsed_footer_line[1]))
3504 return
3505
3506 if not self._description_lines:
3507 self._description_lines.append(line)
3508 return
3509
3510 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3511 if gerrit_footers:
3512 # git_footers.split_footers ensures that there is an empty line before
3513 # actual (gerrit) footers, if any. We have to keep it that way.
3514 assert top_lines and top_lines[-1] == ''
3515 top_lines, separator = top_lines[:-1], top_lines[-1:]
3516 else:
3517 separator = [] # No need for separator if there are no gerrit_footers.
3518
3519 prev_line = top_lines[-1] if top_lines else ''
3520 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3521 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3522 top_lines.append('')
3523 top_lines.append(line)
3524 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003525
tandrii99a72f22016-08-17 14:33:24 -07003526 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003527 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003528 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003529 reviewers = [match.group(2).strip()
3530 for match in matches
3531 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003532 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003533
bradnelsond975b302016-10-23 12:20:23 -07003534 def get_cced(self):
3535 """Retrieves the list of reviewers."""
3536 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3537 cced = [match.group(2).strip() for match in matches if match]
3538 return cleanup_list(cced)
3539
Nodir Turakulov23b82142017-11-16 11:04:25 -08003540 def get_hash_tags(self):
3541 """Extracts and sanitizes a list of Gerrit hashtags."""
3542 subject = (self._description_lines or ('',))[0]
3543 subject = re.sub(
3544 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3545
3546 tags = []
3547 start = 0
3548 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3549 while True:
3550 m = bracket_exp.match(subject, start)
3551 if not m:
3552 break
3553 tags.append(self.sanitize_hash_tag(m.group(1)))
3554 start = m.end()
3555
3556 if not tags:
3557 # Try "Tag: " prefix.
3558 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3559 if m:
3560 tags.append(self.sanitize_hash_tag(m.group(1)))
3561 return tags
3562
3563 @classmethod
3564 def sanitize_hash_tag(cls, tag):
3565 """Returns a sanitized Gerrit hash tag.
3566
3567 A sanitized hashtag can be used as a git push refspec parameter value.
3568 """
3569 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3570
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003571 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3572 """Updates this commit description given the parent.
3573
3574 This is essentially what Gnumbd used to do.
3575 Consult https://goo.gl/WMmpDe for more details.
3576 """
3577 assert parent_msg # No, orphan branch creation isn't supported.
3578 assert parent_hash
3579 assert dest_ref
3580 parent_footer_map = git_footers.parse_footers(parent_msg)
3581 # This will also happily parse svn-position, which GnumbD is no longer
3582 # supporting. While we'd generate correct footers, the verifier plugin
3583 # installed in Gerrit will block such commit (ie git push below will fail).
3584 parent_position = git_footers.get_position(parent_footer_map)
3585
3586 # Cherry-picks may have last line obscuring their prior footers,
3587 # from git_footers perspective. This is also what Gnumbd did.
3588 cp_line = None
3589 if (self._description_lines and
3590 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3591 cp_line = self._description_lines.pop()
3592
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003593 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003594
3595 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3596 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003597 for i, line in enumerate(footer_lines):
3598 k, v = git_footers.parse_footer(line) or (None, None)
3599 if k and k.startswith('Cr-'):
3600 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003601
3602 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003603 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003604 if parent_position[0] == dest_ref:
3605 # Same branch as parent.
3606 number = int(parent_position[1]) + 1
3607 else:
3608 number = 1 # New branch, and extra lineage.
3609 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3610 int(parent_position[1])))
3611
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003612 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3613 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003614
3615 self._description_lines = top_lines
3616 if cp_line:
3617 self._description_lines.append(cp_line)
3618 if self._description_lines[-1] != '':
3619 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003620 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003621
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003622
Aaron Gablea1bab272017-04-11 16:38:18 -07003623def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003624 """Retrieves the reviewers that approved a CL from the issue properties with
3625 messages.
3626
3627 Note that the list may contain reviewers that are not committer, thus are not
3628 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003629
3630 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003631 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003632 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003633 return sorted(
3634 set(
3635 message['sender']
3636 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003637 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003638 )
3639 )
3640
3641
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003642def FindCodereviewSettingsFile(filename='codereview.settings'):
3643 """Finds the given file starting in the cwd and going up.
3644
3645 Only looks up to the top of the repository unless an
3646 'inherit-review-settings-ok' file exists in the root of the repository.
3647 """
3648 inherit_ok_file = 'inherit-review-settings-ok'
3649 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003650 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003651 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3652 root = '/'
3653 while True:
3654 if filename in os.listdir(cwd):
3655 if os.path.isfile(os.path.join(cwd, filename)):
3656 return open(os.path.join(cwd, filename))
3657 if cwd == root:
3658 break
3659 cwd = os.path.dirname(cwd)
3660
3661
3662def LoadCodereviewSettingsFromFile(fileobj):
3663 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003664 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003666 def SetProperty(name, setting, unset_error_ok=False):
3667 fullname = 'rietveld.' + name
3668 if setting in keyvals:
3669 RunGit(['config', fullname, keyvals[setting]])
3670 else:
3671 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3672
tandrii48df5812016-10-17 03:55:37 -07003673 if not keyvals.get('GERRIT_HOST', False):
3674 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675 # Only server setting is required. Other settings can be absent.
3676 # In that case, we ignore errors raised during option deletion attempt.
3677 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003678 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3680 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003681 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003682 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3683 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003684 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003685 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3686 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003688 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003689 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003690
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003691 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003692 RunGit(['config', 'gerrit.squash-uploads',
3693 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003694
tandrii@chromium.org28253532016-04-14 13:46:56 +00003695 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003696 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003697 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3698
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003699 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003700 # should be of the form
3701 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3702 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3704 keyvals['ORIGIN_URL_CONFIG']])
3705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003707def urlretrieve(source, destination):
3708 """urllib is broken for SSL connections via a proxy therefore we
3709 can't use urllib.urlretrieve()."""
3710 with open(destination, 'w') as f:
3711 f.write(urllib2.urlopen(source).read())
3712
3713
ukai@chromium.org712d6102013-11-27 00:52:58 +00003714def hasSheBang(fname):
3715 """Checks fname is a #! script."""
3716 with open(fname) as f:
3717 return f.read(2).startswith('#!')
3718
3719
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003720# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3721def DownloadHooks(*args, **kwargs):
3722 pass
3723
3724
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003725def DownloadGerritHook(force):
3726 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003727
3728 Args:
3729 force: True to update hooks. False to install hooks if not present.
3730 """
3731 if not settings.GetIsGerrit():
3732 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003733 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003734 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3735 if not os.access(dst, os.X_OK):
3736 if os.path.exists(dst):
3737 if not force:
3738 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003739 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003740 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003741 if not hasSheBang(dst):
3742 DieWithError('Not a script: %s\n'
3743 'You need to download from\n%s\n'
3744 'into .git/hooks/commit-msg and '
3745 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003746 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3747 except Exception:
3748 if os.path.exists(dst):
3749 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003750 DieWithError('\nFailed to download hooks.\n'
3751 'You need to download from\n%s\n'
3752 'into .git/hooks/commit-msg and '
3753 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003754
3755
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003756def GetRietveldCodereviewSettingsInteractively():
3757 """Prompt the user for settings."""
3758 server = settings.GetDefaultServerUrl(error_ok=True)
3759 prompt = 'Rietveld server (host[:port])'
3760 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3761 newserver = ask_for_data(prompt + ':')
3762 if not server and not newserver:
3763 newserver = DEFAULT_SERVER
3764 if newserver:
3765 newserver = gclient_utils.UpgradeToHttps(newserver)
3766 if newserver != server:
3767 RunGit(['config', 'rietveld.server', newserver])
3768
3769 def SetProperty(initial, caption, name, is_url):
3770 prompt = caption
3771 if initial:
3772 prompt += ' ("x" to clear) [%s]' % initial
3773 new_val = ask_for_data(prompt + ':')
3774 if new_val == 'x':
3775 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3776 elif new_val:
3777 if is_url:
3778 new_val = gclient_utils.UpgradeToHttps(new_val)
3779 if new_val != initial:
3780 RunGit(['config', 'rietveld.' + name, new_val])
3781
3782 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3783 SetProperty(settings.GetDefaultPrivateFlag(),
3784 'Private flag (rietveld only)', 'private', False)
3785 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3786 'tree-status-url', False)
3787 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3788 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3789 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3790 'run-post-upload-hook', False)
3791
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003792
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003793class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003794 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003795
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003796 _GOOGLESOURCE = 'googlesource.com'
3797
3798 def __init__(self):
3799 # Cached list of [host, identity, source], where source is either
3800 # .gitcookies or .netrc.
3801 self._all_hosts = None
3802
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003803 def ensure_configured_gitcookies(self):
3804 """Runs checks and suggests fixes to make git use .gitcookies from default
3805 path."""
3806 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3807 configured_path = RunGitSilent(
3808 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003809 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003810 if configured_path:
3811 self._ensure_default_gitcookies_path(configured_path, default)
3812 else:
3813 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003814
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003815 @staticmethod
3816 def _ensure_default_gitcookies_path(configured_path, default_path):
3817 assert configured_path
3818 if configured_path == default_path:
3819 print('git is already configured to use your .gitcookies from %s' %
3820 configured_path)
3821 return
3822
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003823 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003824 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3825 (configured_path, default_path))
3826
3827 if not os.path.exists(configured_path):
3828 print('However, your configured .gitcookies file is missing.')
3829 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3830 action='reconfigure')
3831 RunGit(['config', '--global', 'http.cookiefile', default_path])
3832 return
3833
3834 if os.path.exists(default_path):
3835 print('WARNING: default .gitcookies file already exists %s' %
3836 default_path)
3837 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3838 default_path)
3839
3840 confirm_or_exit('Move existing .gitcookies to default location?',
3841 action='move')
3842 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003843 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003844 print('Moved and reconfigured git to use .gitcookies from %s' %
3845 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003846
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003847 @staticmethod
3848 def _configure_gitcookies_path(default_path):
3849 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3850 if os.path.exists(netrc_path):
3851 print('You seem to be using outdated .netrc for git credentials: %s' %
3852 netrc_path)
3853 print('This tool will guide you through setting up recommended '
3854 '.gitcookies store for git credentials.\n'
3855 '\n'
3856 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3857 ' git config --global --unset http.cookiefile\n'
3858 ' mv %s %s.backup\n\n' % (default_path, default_path))
3859 confirm_or_exit(action='setup .gitcookies')
3860 RunGit(['config', '--global', 'http.cookiefile', default_path])
3861 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003862
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003863 def get_hosts_with_creds(self, include_netrc=False):
3864 if self._all_hosts is None:
3865 a = gerrit_util.CookiesAuthenticator()
3866 self._all_hosts = [
3867 (h, u, s)
3868 for h, u, s in itertools.chain(
3869 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3870 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3871 )
3872 if h.endswith(self._GOOGLESOURCE)
3873 ]
3874
3875 if include_netrc:
3876 return self._all_hosts
3877 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3878
3879 def print_current_creds(self, include_netrc=False):
3880 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3881 if not hosts:
3882 print('No Git/Gerrit credentials found')
3883 return
3884 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3885 header = [('Host', 'User', 'Which file'),
3886 ['=' * l for l in lengths]]
3887 for row in (header + hosts):
3888 print('\t'.join((('%%+%ds' % l) % s)
3889 for l, s in zip(lengths, row)))
3890
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003891 @staticmethod
3892 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003893 """Parses identity "git-<username>.domain" into <username> and domain."""
3894 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003895 # distinguishable from sub-domains. But we do know typical domains:
3896 if identity.endswith('.chromium.org'):
3897 domain = 'chromium.org'
3898 username = identity[:-len('.chromium.org')]
3899 else:
3900 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003901 if username.startswith('git-'):
3902 username = username[len('git-'):]
3903 return username, domain
3904
3905 def _get_usernames_of_domain(self, domain):
3906 """Returns list of usernames referenced by .gitcookies in a given domain."""
3907 identities_by_domain = {}
3908 for _, identity, _ in self.get_hosts_with_creds():
3909 username, domain = self._parse_identity(identity)
3910 identities_by_domain.setdefault(domain, []).append(username)
3911 return identities_by_domain.get(domain)
3912
3913 def _canonical_git_googlesource_host(self, host):
3914 """Normalizes Gerrit hosts (with '-review') to Git host."""
3915 assert host.endswith(self._GOOGLESOURCE)
3916 # Prefix doesn't include '.' at the end.
3917 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3918 if prefix.endswith('-review'):
3919 prefix = prefix[:-len('-review')]
3920 return prefix + '.' + self._GOOGLESOURCE
3921
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003922 def _canonical_gerrit_googlesource_host(self, host):
3923 git_host = self._canonical_git_googlesource_host(host)
3924 prefix = git_host.split('.', 1)[0]
3925 return prefix + '-review.' + self._GOOGLESOURCE
3926
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003927 def _get_counterpart_host(self, host):
3928 assert host.endswith(self._GOOGLESOURCE)
3929 git = self._canonical_git_googlesource_host(host)
3930 gerrit = self._canonical_gerrit_googlesource_host(git)
3931 return git if gerrit == host else gerrit
3932
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003933 def has_generic_host(self):
3934 """Returns whether generic .googlesource.com has been configured.
3935
3936 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3937 """
3938 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3939 if host == '.' + self._GOOGLESOURCE:
3940 return True
3941 return False
3942
3943 def _get_git_gerrit_identity_pairs(self):
3944 """Returns map from canonic host to pair of identities (Git, Gerrit).
3945
3946 One of identities might be None, meaning not configured.
3947 """
3948 host_to_identity_pairs = {}
3949 for host, identity, _ in self.get_hosts_with_creds():
3950 canonical = self._canonical_git_googlesource_host(host)
3951 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3952 idx = 0 if canonical == host else 1
3953 pair[idx] = identity
3954 return host_to_identity_pairs
3955
3956 def get_partially_configured_hosts(self):
3957 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003958 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3959 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3960 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003961
3962 def get_conflicting_hosts(self):
3963 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003964 host
3965 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003966 if None not in (i1, i2) and i1 != i2)
3967
3968 def get_duplicated_hosts(self):
3969 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3970 return set(host for host, count in counters.iteritems() if count > 1)
3971
3972 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3973 'chromium.googlesource.com': 'chromium.org',
3974 'chrome-internal.googlesource.com': 'google.com',
3975 }
3976
3977 def get_hosts_with_wrong_identities(self):
3978 """Finds hosts which **likely** reference wrong identities.
3979
3980 Note: skips hosts which have conflicting identities for Git and Gerrit.
3981 """
3982 hosts = set()
3983 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3984 pair = self._get_git_gerrit_identity_pairs().get(host)
3985 if pair and pair[0] == pair[1]:
3986 _, domain = self._parse_identity(pair[0])
3987 if domain != expected:
3988 hosts.add(host)
3989 return hosts
3990
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003991 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003992 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003993 hosts = sorted(hosts)
3994 assert hosts
3995 if extra_column_func is None:
3996 extras = [''] * len(hosts)
3997 else:
3998 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003999 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4000 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004001 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004002 lines.append(tmpl % he)
4003 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004004
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004005 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004006 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004007 yield ('.googlesource.com wildcard record detected',
4008 ['Chrome Infrastructure team recommends to list full host names '
4009 'explicitly.'],
4010 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004011
4012 dups = self.get_duplicated_hosts()
4013 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004014 yield ('The following hosts were defined twice',
4015 self._format_hosts(dups),
4016 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004017
4018 partial = self.get_partially_configured_hosts()
4019 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004020 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4021 'These hosts are missing',
4022 self._format_hosts(partial, lambda host: 'but %s defined' %
4023 self._get_counterpart_host(host)),
4024 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004025
4026 conflicting = self.get_conflicting_hosts()
4027 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004028 yield ('The following Git hosts have differing credentials from their '
4029 'Gerrit counterparts',
4030 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4031 tuple(self._get_git_gerrit_identity_pairs()[host])),
4032 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004033
4034 wrong = self.get_hosts_with_wrong_identities()
4035 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004036 yield ('These hosts likely use wrong identity',
4037 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4038 (self._get_git_gerrit_identity_pairs()[host][0],
4039 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4040 wrong)
4041
4042 def find_and_report_problems(self):
4043 """Returns True if there was at least one problem, else False."""
4044 found = False
4045 bad_hosts = set()
4046 for title, sublines, hosts in self._find_problems():
4047 if not found:
4048 found = True
4049 print('\n\n.gitcookies problem report:\n')
4050 bad_hosts.update(hosts or [])
4051 print(' %s%s' % (title , (':' if sublines else '')))
4052 if sublines:
4053 print()
4054 print(' %s' % '\n '.join(sublines))
4055 print()
4056
4057 if bad_hosts:
4058 assert found
4059 print(' You can manually remove corresponding lines in your %s file and '
4060 'visit the following URLs with correct account to generate '
4061 'correct credential lines:\n' %
4062 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4063 print(' %s' % '\n '.join(sorted(set(
4064 gerrit_util.CookiesAuthenticator().get_new_password_url(
4065 self._canonical_git_googlesource_host(host))
4066 for host in bad_hosts
4067 ))))
4068 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004069
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004070
4071def CMDcreds_check(parser, args):
4072 """Checks credentials and suggests changes."""
4073 _, _ = parser.parse_args(args)
4074
4075 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004076 DieWithError(
4077 'This command is not designed for GCE, are you on a bot?\n'
4078 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004079
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004080 checker = _GitCookiesChecker()
4081 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004082
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004083 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004084 checker.print_current_creds(include_netrc=True)
4085
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004086 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004087 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004088 return 0
4089 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004090
4091
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004092@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004094 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004096 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004097 # TODO(tandrii): remove this once we switch to Gerrit.
4098 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004099 parser.add_option('--activate-update', action='store_true',
4100 help='activate auto-updating [rietveld] section in '
4101 '.git/config')
4102 parser.add_option('--deactivate-update', action='store_true',
4103 help='deactivate auto-updating [rietveld] section in '
4104 '.git/config')
4105 options, args = parser.parse_args(args)
4106
4107 if options.deactivate_update:
4108 RunGit(['config', 'rietveld.autoupdate', 'false'])
4109 return
4110
4111 if options.activate_update:
4112 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4113 return
4114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004116 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004117 return 0
4118
4119 url = args[0]
4120 if not url.endswith('codereview.settings'):
4121 url = os.path.join(url, 'codereview.settings')
4122
4123 # Load code review settings and download hooks (if available).
4124 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4125 return 0
4126
4127
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004128def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004129 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004130 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4131 branch = ShortBranchName(branchref)
4132 _, args = parser.parse_args(args)
4133 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004134 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004135 return RunGit(['config', 'branch.%s.base-url' % branch],
4136 error_ok=False).strip()
4137 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004138 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004139 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4140 error_ok=False).strip()
4141
4142
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004143def color_for_status(status):
4144 """Maps a Changelist status to color, for CMDstatus and other tools."""
4145 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004146 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004147 'waiting': Fore.BLUE,
4148 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004149 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004150 'lgtm': Fore.GREEN,
4151 'commit': Fore.MAGENTA,
4152 'closed': Fore.CYAN,
4153 'error': Fore.WHITE,
4154 }.get(status, Fore.WHITE)
4155
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004156
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004157def get_cl_statuses(changes, fine_grained, max_processes=None):
4158 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004159
4160 If fine_grained is true, this will fetch CL statuses from the server.
4161 Otherwise, simply indicate if there's a matching url for the given branches.
4162
4163 If max_processes is specified, it is used as the maximum number of processes
4164 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4165 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004166
4167 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004168 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004169 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004170 upload.verbosity = 0
4171
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004172 if not changes:
4173 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004174
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004175 if not fine_grained:
4176 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004177 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004178 for cl in changes:
4179 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004180 return
4181
4182 # First, sort out authentication issues.
4183 logging.debug('ensuring credentials exist')
4184 for cl in changes:
4185 cl.EnsureAuthenticated(force=False, refresh=True)
4186
4187 def fetch(cl):
4188 try:
4189 return (cl, cl.GetStatus())
4190 except:
4191 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004192 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004193 raise
4194
4195 threads_count = len(changes)
4196 if max_processes:
4197 threads_count = max(1, min(threads_count, max_processes))
4198 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4199
4200 pool = ThreadPool(threads_count)
4201 fetched_cls = set()
4202 try:
4203 it = pool.imap_unordered(fetch, changes).__iter__()
4204 while True:
4205 try:
4206 cl, status = it.next(timeout=5)
4207 except multiprocessing.TimeoutError:
4208 break
4209 fetched_cls.add(cl)
4210 yield cl, status
4211 finally:
4212 pool.close()
4213
4214 # Add any branches that failed to fetch.
4215 for cl in set(changes) - fetched_cls:
4216 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004217
rmistry@google.com2dd99862015-06-22 12:22:18 +00004218
4219def upload_branch_deps(cl, args):
4220 """Uploads CLs of local branches that are dependents of the current branch.
4221
4222 If the local branch dependency tree looks like:
4223 test1 -> test2.1 -> test3.1
4224 -> test3.2
4225 -> test2.2 -> test3.3
4226
4227 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4228 run on the dependent branches in this order:
4229 test2.1, test3.1, test3.2, test2.2, test3.3
4230
4231 Note: This function does not rebase your local dependent branches. Use it when
4232 you make a change to the parent branch that will not conflict with its
4233 dependent branches, and you would like their dependencies updated in
4234 Rietveld.
4235 """
4236 if git_common.is_dirty_git_tree('upload-branch-deps'):
4237 return 1
4238
4239 root_branch = cl.GetBranch()
4240 if root_branch is None:
4241 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4242 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004243 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004244 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4245 'patchset dependencies without an uploaded CL.')
4246
4247 branches = RunGit(['for-each-ref',
4248 '--format=%(refname:short) %(upstream:short)',
4249 'refs/heads'])
4250 if not branches:
4251 print('No local branches found.')
4252 return 0
4253
4254 # Create a dictionary of all local branches to the branches that are dependent
4255 # on it.
4256 tracked_to_dependents = collections.defaultdict(list)
4257 for b in branches.splitlines():
4258 tokens = b.split()
4259 if len(tokens) == 2:
4260 branch_name, tracked = tokens
4261 tracked_to_dependents[tracked].append(branch_name)
4262
vapiera7fbd5a2016-06-16 09:17:49 -07004263 print()
4264 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004265 dependents = []
4266 def traverse_dependents_preorder(branch, padding=''):
4267 dependents_to_process = tracked_to_dependents.get(branch, [])
4268 padding += ' '
4269 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004270 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004271 dependents.append(dependent)
4272 traverse_dependents_preorder(dependent, padding)
4273 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004274 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004275
4276 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004277 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004278 return 0
4279
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004280 confirm_or_exit('This command will checkout all dependent branches and run '
4281 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004282
andybons@chromium.org962f9462016-02-03 20:00:42 +00004283 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004284 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004285 args.extend(['-t', 'Updated patchset dependency'])
4286
rmistry@google.com2dd99862015-06-22 12:22:18 +00004287 # Record all dependents that failed to upload.
4288 failures = {}
4289 # Go through all dependents, checkout the branch and upload.
4290 try:
4291 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print()
4293 print('--------------------------------------')
4294 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004295 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004297 try:
4298 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004300 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004301 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004302 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004303 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004304 finally:
4305 # Swap back to the original root branch.
4306 RunGit(['checkout', '-q', root_branch])
4307
vapiera7fbd5a2016-06-16 09:17:49 -07004308 print()
4309 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004310 for dependent_branch in dependents:
4311 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004312 print(' %s : %s' % (dependent_branch, upload_status))
4313 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004314
4315 return 0
4316
4317
kmarshall3bff56b2016-06-06 18:31:47 -07004318def CMDarchive(parser, args):
4319 """Archives and deletes branches associated with closed changelists."""
4320 parser.add_option(
4321 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004322 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004323 parser.add_option(
4324 '-f', '--force', action='store_true',
4325 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004326 parser.add_option(
4327 '-d', '--dry-run', action='store_true',
4328 help='Skip the branch tagging and removal steps.')
4329 parser.add_option(
4330 '-t', '--notags', action='store_true',
4331 help='Do not tag archived branches. '
4332 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004333
4334 auth.add_auth_options(parser)
4335 options, args = parser.parse_args(args)
4336 if args:
4337 parser.error('Unsupported args: %s' % ' '.join(args))
4338 auth_config = auth.extract_auth_config_from_options(options)
4339
4340 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4341 if not branches:
4342 return 0
4343
vapiera7fbd5a2016-06-16 09:17:49 -07004344 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004345 changes = [Changelist(branchref=b, auth_config=auth_config)
4346 for b in branches.splitlines()]
4347 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4348 statuses = get_cl_statuses(changes,
4349 fine_grained=True,
4350 max_processes=options.maxjobs)
4351 proposal = [(cl.GetBranch(),
4352 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4353 for cl, status in statuses
4354 if status == 'closed']
4355 proposal.sort()
4356
4357 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004358 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004359 return 0
4360
4361 current_branch = GetCurrentBranch()
4362
vapiera7fbd5a2016-06-16 09:17:49 -07004363 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004364 if options.notags:
4365 for next_item in proposal:
4366 print(' ' + next_item[0])
4367 else:
4368 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4369 for next_item in proposal:
4370 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004371
kmarshall9249e012016-08-23 12:02:16 -07004372 # Quit now on precondition failure or if instructed by the user, either
4373 # via an interactive prompt or by command line flags.
4374 if options.dry_run:
4375 print('\nNo changes were made (dry run).\n')
4376 return 0
4377 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004378 print('You are currently on a branch \'%s\' which is associated with a '
4379 'closed codereview issue, so archive cannot proceed. Please '
4380 'checkout another branch and run this command again.' %
4381 current_branch)
4382 return 1
kmarshall9249e012016-08-23 12:02:16 -07004383 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004384 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4385 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004386 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004387 return 1
4388
4389 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004390 if not options.notags:
4391 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004392 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004393
vapiera7fbd5a2016-06-16 09:17:49 -07004394 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004395
4396 return 0
4397
4398
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004400 """Show status of changelists.
4401
4402 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004403 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004404 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004405 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004406 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004407 - Magenta in the commit queue
4408 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004409 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004410
4411 Also see 'git cl comments'.
4412 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004413 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004414 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004415 parser.add_option('-f', '--fast', action='store_true',
4416 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004417 parser.add_option(
4418 '-j', '--maxjobs', action='store', type=int,
4419 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004420
4421 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004422 _add_codereview_issue_select_options(
4423 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004424 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004425 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004426 if args:
4427 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004428 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004429
iannuccie53c9352016-08-17 14:40:40 -07004430 if options.issue is not None and not options.field:
4431 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004433 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004434 cl = Changelist(auth_config=auth_config, issue=options.issue,
4435 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004437 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004438 elif options.field == 'id':
4439 issueid = cl.GetIssue()
4440 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004441 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004442 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004443 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004444 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004445 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004446 elif options.field == 'status':
4447 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004448 elif options.field == 'url':
4449 url = cl.GetIssueURL()
4450 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004451 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004452 return 0
4453
4454 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4455 if not branches:
4456 print('No local branch found.')
4457 return 0
4458
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004459 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004460 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004461 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004462 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004463 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004464 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004465 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004466
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004467 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004468 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4469 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4470 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004471 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004472 c, status = output.next()
4473 branch_statuses[c.GetBranch()] = status
4474 status = branch_statuses.pop(branch)
4475 url = cl.GetIssueURL()
4476 if url and (not status or status == 'error'):
4477 # The issue probably doesn't exist anymore.
4478 url += ' (broken)'
4479
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004480 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004481 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004482 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004483 color = ''
4484 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004485 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004486 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004487 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004488 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004489
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004490
4491 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004492 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004493 print('Current branch: %s' % branch)
4494 for cl in changes:
4495 if cl.GetBranch() == branch:
4496 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004497 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004498 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004499 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004500 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004501 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004502 print('Issue description:')
4503 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504 return 0
4505
4506
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004507def colorize_CMDstatus_doc():
4508 """To be called once in main() to add colors to git cl status help."""
4509 colors = [i for i in dir(Fore) if i[0].isupper()]
4510
4511 def colorize_line(line):
4512 for color in colors:
4513 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004514 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004515 indent = len(line) - len(line.lstrip(' ')) + 1
4516 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4517 return line
4518
4519 lines = CMDstatus.__doc__.splitlines()
4520 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4521
4522
phajdan.jre328cf92016-08-22 04:12:17 -07004523def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004524 if path == '-':
4525 json.dump(contents, sys.stdout)
4526 else:
4527 with open(path, 'w') as f:
4528 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004529
4530
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004531@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004533 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004534
4535 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004536 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004537 parser.add_option('-r', '--reverse', action='store_true',
4538 help='Lookup the branch(es) for the specified issues. If '
4539 'no issues are specified, all branches with mapped '
4540 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004541 parser.add_option('--json',
4542 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004543 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004544 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004545 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004546
dnj@chromium.org406c4402015-03-03 17:22:28 +00004547 if options.reverse:
4548 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004549 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004550 # Reverse issue lookup.
4551 issue_branch_map = {}
4552 for branch in branches:
4553 cl = Changelist(branchref=branch)
4554 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4555 if not args:
4556 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004557 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004558 for issue in args:
4559 if not issue:
4560 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004561 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004562 print('Branch for issue number %s: %s' % (
4563 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004564 if options.json:
4565 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004566 return 0
4567
4568 if len(args) > 0:
4569 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4570 if not issue.valid:
4571 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4572 'or no argument to list it.\n'
4573 'Maybe you want to run git cl status?')
4574 cl = Changelist(codereview=issue.codereview)
4575 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004576 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004577 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004578 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4579 if options.json:
4580 write_json(options.json, {
4581 'issue': cl.GetIssue(),
4582 'issue_url': cl.GetIssueURL(),
4583 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004584 return 0
4585
4586
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004587def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004588 """Shows or posts review comments for any changelist."""
4589 parser.add_option('-a', '--add-comment', dest='comment',
4590 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004591 parser.add_option('-i', '--issue', dest='issue',
4592 help='review issue id (defaults to current issue). '
4593 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004594 parser.add_option('-m', '--machine-readable', dest='readable',
4595 action='store_false', default=True,
4596 help='output comments in a format compatible with '
4597 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004598 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004599 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004600 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004601 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004602 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004603 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004604 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004605
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004606 issue = None
4607 if options.issue:
4608 try:
4609 issue = int(options.issue)
4610 except ValueError:
4611 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004612 if not options.forced_codereview:
4613 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004614
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004615 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004616 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004617 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004618
4619 if options.comment:
4620 cl.AddComment(options.comment)
4621 return 0
4622
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004623 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4624 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004625 for comment in summary:
4626 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004627 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004628 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004629 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004630 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004631 color = Fore.MAGENTA
4632 else:
4633 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004634 print('\n%s%s %s%s\n%s' % (
4635 color,
4636 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4637 comment.sender,
4638 Fore.RESET,
4639 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4640
smut@google.comc85ac942015-09-15 16:34:43 +00004641 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004642 def pre_serialize(c):
4643 dct = c.__dict__.copy()
4644 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4645 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004646 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004647 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004648 return 0
4649
4650
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004651@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004652def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004653 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004654 parser.add_option('-d', '--display', action='store_true',
4655 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004656 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004657 help='New description to set for this issue (- for stdin, '
4658 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004659 parser.add_option('-f', '--force', action='store_true',
4660 help='Delete any unpublished Gerrit edits for this issue '
4661 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004662
4663 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004664 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004665 options, args = parser.parse_args(args)
4666 _process_codereview_select_options(parser, options)
4667
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004668 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004669 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004670 target_issue_arg = ParseIssueNumberArgument(args[0],
4671 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004672 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004673 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004674
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004675 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004676
martiniss6eda05f2016-06-30 10:18:35 -07004677 kwargs = {
4678 'auth_config': auth_config,
4679 'codereview': options.forced_codereview,
4680 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004681 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004682 if target_issue_arg:
4683 kwargs['issue'] = target_issue_arg.issue
4684 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004685 if target_issue_arg.codereview and not options.forced_codereview:
4686 detected_codereview_from_url = True
4687 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004688
4689 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004690 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004691 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004692 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004693
4694 if detected_codereview_from_url:
4695 logging.info('canonical issue/change URL: %s (type: %s)\n',
4696 cl.GetIssueURL(), target_issue_arg.codereview)
4697
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004698 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004699
smut@google.com34fb6b12015-07-13 20:03:26 +00004700 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004701 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004702 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004703
4704 if options.new_description:
4705 text = options.new_description
4706 if text == '-':
4707 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004708 elif text == '+':
4709 base_branch = cl.GetCommonAncestorWithUpstream()
4710 change = cl.GetChange(base_branch, None, local_description=True)
4711 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004712
4713 description.set_description(text)
4714 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004715 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004716
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004717 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004718 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004719 return 0
4720
4721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004722def CreateDescriptionFromLog(args):
4723 """Pulls out the commit log to use as a base for the CL description."""
4724 log_args = []
4725 if len(args) == 1 and not args[0].endswith('.'):
4726 log_args = [args[0] + '..']
4727 elif len(args) == 1 and args[0].endswith('...'):
4728 log_args = [args[0][:-1]]
4729 elif len(args) == 2:
4730 log_args = [args[0] + '..' + args[1]]
4731 else:
4732 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004733 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734
4735
thestig@chromium.org44202a22014-03-11 19:22:18 +00004736def CMDlint(parser, args):
4737 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004738 parser.add_option('--filter', action='append', metavar='-x,+y',
4739 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004740 auth.add_auth_options(parser)
4741 options, args = parser.parse_args(args)
4742 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004743
4744 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004745 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004746 try:
4747 import cpplint
4748 import cpplint_chromium
4749 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004750 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004751 return 1
4752
4753 # Change the current working directory before calling lint so that it
4754 # shows the correct base.
4755 previous_cwd = os.getcwd()
4756 os.chdir(settings.GetRoot())
4757 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004758 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004759 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4760 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004761 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004762 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004763 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004764
4765 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004766 command = args + files
4767 if options.filter:
4768 command = ['--filter=' + ','.join(options.filter)] + command
4769 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004770
4771 white_regex = re.compile(settings.GetLintRegex())
4772 black_regex = re.compile(settings.GetLintIgnoreRegex())
4773 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4774 for filename in filenames:
4775 if white_regex.match(filename):
4776 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004777 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004778 else:
4779 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4780 extra_check_functions)
4781 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004782 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004783 finally:
4784 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004785 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004786 if cpplint._cpplint_state.error_count != 0:
4787 return 1
4788 return 0
4789
4790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004791def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004792 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004793 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004794 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004795 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004796 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004797 parser.add_option('--all', action='store_true',
4798 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004799 parser.add_option('--parallel', action='store_true',
4800 help='Run all tests specified by input_api.RunTests in all '
4801 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004802 auth.add_auth_options(parser)
4803 options, args = parser.parse_args(args)
4804 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004805
sbc@chromium.org71437c02015-04-09 19:29:40 +00004806 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004807 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004808 return 1
4809
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004810 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004811 if args:
4812 base_branch = args[0]
4813 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004814 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004815 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004816
Aaron Gable8076c282017-11-29 14:39:41 -08004817 if options.all:
4818 base_change = cl.GetChange(base_branch, None)
4819 files = [('M', f) for f in base_change.AllFiles()]
4820 change = presubmit_support.GitChange(
4821 base_change.Name(),
4822 base_change.FullDescriptionText(),
4823 base_change.RepositoryRoot(),
4824 files,
4825 base_change.issue,
4826 base_change.patchset,
4827 base_change.author_email,
4828 base_change._upstream)
4829 else:
4830 change = cl.GetChange(base_branch, None)
4831
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004832 cl.RunHook(
4833 committing=not options.upload,
4834 may_prompt=False,
4835 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004836 change=change,
4837 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004838 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004839
4840
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004841def GenerateGerritChangeId(message):
4842 """Returns Ixxxxxx...xxx change id.
4843
4844 Works the same way as
4845 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4846 but can be called on demand on all platforms.
4847
4848 The basic idea is to generate git hash of a state of the tree, original commit
4849 message, author/committer info and timestamps.
4850 """
4851 lines = []
4852 tree_hash = RunGitSilent(['write-tree'])
4853 lines.append('tree %s' % tree_hash.strip())
4854 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4855 if code == 0:
4856 lines.append('parent %s' % parent.strip())
4857 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4858 lines.append('author %s' % author.strip())
4859 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4860 lines.append('committer %s' % committer.strip())
4861 lines.append('')
4862 # Note: Gerrit's commit-hook actually cleans message of some lines and
4863 # whitespace. This code is not doing this, but it clearly won't decrease
4864 # entropy.
4865 lines.append(message)
4866 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4867 stdin='\n'.join(lines))
4868 return 'I%s' % change_hash.strip()
4869
4870
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004871def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004872 """Computes the remote branch ref to use for the CL.
4873
4874 Args:
4875 remote (str): The git remote for the CL.
4876 remote_branch (str): The git remote branch for the CL.
4877 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004878 """
4879 if not (remote and remote_branch):
4880 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004881
wittman@chromium.org455dc922015-01-26 20:15:50 +00004882 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004883 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004884 # refs, which are then translated into the remote full symbolic refs
4885 # below.
4886 if '/' not in target_branch:
4887 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4888 else:
4889 prefix_replacements = (
4890 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4891 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4892 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4893 )
4894 match = None
4895 for regex, replacement in prefix_replacements:
4896 match = re.search(regex, target_branch)
4897 if match:
4898 remote_branch = target_branch.replace(match.group(0), replacement)
4899 break
4900 if not match:
4901 # This is a branch path but not one we recognize; use as-is.
4902 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004903 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4904 # Handle the refs that need to land in different refs.
4905 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004906
wittman@chromium.org455dc922015-01-26 20:15:50 +00004907 # Create the true path to the remote branch.
4908 # Does the following translation:
4909 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4910 # * refs/remotes/origin/master -> refs/heads/master
4911 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4912 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4913 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4914 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4915 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4916 'refs/heads/')
4917 elif remote_branch.startswith('refs/remotes/branch-heads'):
4918 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004919
wittman@chromium.org455dc922015-01-26 20:15:50 +00004920 return remote_branch
4921
4922
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004923def cleanup_list(l):
4924 """Fixes a list so that comma separated items are put as individual items.
4925
4926 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4927 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4928 """
4929 items = sum((i.split(',') for i in l), [])
4930 stripped_items = (i.strip() for i in items)
4931 return sorted(filter(None, stripped_items))
4932
4933
Aaron Gable4db38df2017-11-03 14:59:07 -07004934@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004935def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004936 """Uploads the current changelist to codereview.
4937
4938 Can skip dependency patchset uploads for a branch by running:
4939 git config branch.branch_name.skip-deps-uploads True
4940 To unset run:
4941 git config --unset branch.branch_name.skip-deps-uploads
4942 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004943
4944 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4945 a bug number, this bug number is automatically populated in the CL
4946 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004947
4948 If subject contains text in square brackets or has "<text>: " prefix, such
4949 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4950 [git-cl] add support for hashtags
4951 Foo bar: implement foo
4952 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004953 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004954 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4955 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004956 parser.add_option('--bypass-watchlists', action='store_true',
4957 dest='bypass_watchlists',
4958 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004959 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004960 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004961 parser.add_option('--message', '-m', dest='message',
4962 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004963 parser.add_option('-b', '--bug',
4964 help='pre-populate the bug number(s) for this issue. '
4965 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004966 parser.add_option('--message-file', dest='message_file',
4967 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004968 parser.add_option('--title', '-t', dest='title',
4969 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004970 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004971 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004972 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004973 parser.add_option('--tbrs',
4974 action='append', default=[],
4975 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004976 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004977 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004978 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004979 parser.add_option('--hashtag', dest='hashtags',
4980 action='append', default=[],
4981 help=('Gerrit hashtag for new CL; '
4982 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004983 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004984 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004985 parser.add_option('--emulate_svn_auto_props',
4986 '--emulate-svn-auto-props',
4987 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004988 dest="emulate_svn_auto_props",
4989 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004990 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004991 help='tell the commit queue to commit this patchset; '
4992 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004993 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004994 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004995 metavar='TARGET',
4996 help='Apply CL to remote ref TARGET. ' +
4997 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004998 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004999 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005000 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005001 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005002 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005003 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005004 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5005 const='TBR', help='add a set of OWNERS to TBR')
5006 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5007 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005008 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5009 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005010 help='Send the patchset to do a CQ dry run right after '
5011 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005012 parser.add_option('--dependencies', action='store_true',
5013 help='Uploads CLs of all the local branches that depend on '
5014 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005015 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5016 help='Sends your change to the CQ after an approval. Only '
5017 'works on repos that have the Auto-Submit label '
5018 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005019 parser.add_option('--parallel', action='store_true',
5020 help='Run all tests specified by input_api.RunTests in all '
5021 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005022
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005023 # TODO: remove Rietveld flags
5024 parser.add_option('--private', action='store_true',
5025 help='set the review private (rietveld only)')
5026 parser.add_option('--email', default=None,
5027 help='email address to use to connect to Rietveld')
5028
rmistry@google.com2dd99862015-06-22 12:22:18 +00005029 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005030 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005031 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005032 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005033 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005034 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005035
sbc@chromium.org71437c02015-04-09 19:29:40 +00005036 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005037 return 1
5038
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005039 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005040 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005041 options.cc = cleanup_list(options.cc)
5042
tandriib80458a2016-06-23 12:20:07 -07005043 if options.message_file:
5044 if options.message:
5045 parser.error('only one of --message and --message-file allowed.')
5046 options.message = gclient_utils.FileRead(options.message_file)
5047 options.message_file = None
5048
tandrii4d0545a2016-07-06 03:56:49 -07005049 if options.cq_dry_run and options.use_commit_queue:
5050 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5051
Aaron Gableedbc4132017-09-11 13:22:28 -07005052 if options.use_commit_queue:
5053 options.send_mail = True
5054
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005055 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5056 settings.GetIsGerrit()
5057
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005058 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005059 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005060
5061
Francois Dorayd42c6812017-05-30 15:10:20 -04005062@subcommand.usage('--description=<description file>')
5063def CMDsplit(parser, args):
5064 """Splits a branch into smaller branches and uploads CLs.
5065
5066 Creates a branch and uploads a CL for each group of files modified in the
5067 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005068 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005069 the shared OWNERS file.
5070 """
5071 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005072 help="A text file containing a CL description in which "
5073 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005074 parser.add_option("-c", "--comment", dest="comment_file",
5075 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005076 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5077 default=False,
5078 help="List the files and reviewers for each CL that would "
5079 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005080 options, _ = parser.parse_args(args)
5081
5082 if not options.description_file:
5083 parser.error('No --description flag specified.')
5084
5085 def WrappedCMDupload(args):
5086 return CMDupload(OptionParser(), args)
5087
5088 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005089 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005090
5091
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005092@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005093def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005094 """DEPRECATED: Used to commit the current changelist via git-svn."""
5095 message = ('git-cl no longer supports committing to SVN repositories via '
5096 'git-svn. You probably want to use `git cl land` instead.')
5097 print(message)
5098 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005099
5100
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005101# Two special branches used by git cl land.
5102MERGE_BRANCH = 'git-cl-commit'
5103CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5104
5105
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005106@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005107def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005108 """Commits the current changelist via git.
5109
5110 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5111 upstream and closes the issue automatically and atomically.
5112
5113 Otherwise (in case of Rietveld):
5114 Squashes branch into a single commit.
5115 Updates commit message with metadata (e.g. pointer to review).
5116 Pushes the code upstream.
5117 Updates review and closes.
5118 """
5119 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5120 help='bypass upload presubmit hook')
5121 parser.add_option('-m', dest='message',
5122 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005123 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005124 help="force yes to questions (don't prompt)")
5125 parser.add_option('-c', dest='contributor',
5126 help="external contributor for patch (appended to " +
5127 "description and used as author for git). Should be " +
5128 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005129 parser.add_option('--parallel', action='store_true',
5130 help='Run all tests specified by input_api.RunTests in all '
5131 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005132 auth.add_auth_options(parser)
5133 (options, args) = parser.parse_args(args)
5134 auth_config = auth.extract_auth_config_from_options(options)
5135
5136 cl = Changelist(auth_config=auth_config)
5137
Robert Iannucci2e73d432018-03-14 01:10:47 -07005138 if not cl.IsGerrit():
5139 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005140
Robert Iannucci2e73d432018-03-14 01:10:47 -07005141 if options.message:
5142 # This could be implemented, but it requires sending a new patch to
5143 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5144 # Besides, Gerrit has the ability to change the commit message on submit
5145 # automatically, thus there is no need to support this option (so far?).
5146 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005147 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005148 parser.error(
5149 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5150 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5151 'the contributor\'s "name <email>". If you can\'t upload such a '
5152 'commit for review, contact your repository admin and request'
5153 '"Forge-Author" permission.')
5154 if not cl.GetIssue():
5155 DieWithError('You must upload the change first to Gerrit.\n'
5156 ' If you would rather have `git cl land` upload '
5157 'automatically for you, see http://crbug.com/642759')
5158 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005159 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005160
5161
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005162def PushToGitWithAutoRebase(remote, branch, original_description,
5163 git_numberer_enabled, max_attempts=3):
5164 """Pushes current HEAD commit on top of remote's branch.
5165
5166 Attempts to fetch and autorebase on push failures.
5167 Adds git number footers on the fly.
5168
5169 Returns integer code from last command.
5170 """
5171 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5172 code = 0
5173 attempts_left = max_attempts
5174 while attempts_left:
5175 attempts_left -= 1
5176 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5177
5178 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5179 # If fetch fails, retry.
5180 print('Fetching %s/%s...' % (remote, branch))
5181 code, out = RunGitWithCode(
5182 ['retry', 'fetch', remote,
5183 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5184 if code:
5185 print('Fetch failed with exit code %d.' % code)
5186 print(out.strip())
5187 continue
5188
5189 print('Cherry-picking commit on top of latest %s' % branch)
5190 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5191 suppress_stderr=True)
5192 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5193 code, out = RunGitWithCode(['cherry-pick', cherry])
5194 if code:
5195 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5196 'the following files have merge conflicts:' %
5197 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005198 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5199 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005200 print('Please rebase your patch and try again.')
5201 RunGitWithCode(['cherry-pick', '--abort'])
5202 break
5203
5204 commit_desc = ChangeDescription(original_description)
5205 if git_numberer_enabled:
5206 logging.debug('Adding git number footers')
5207 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5208 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5209 branch)
5210 # Ensure timestamps are monotonically increasing.
5211 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5212 _get_committer_timestamp('HEAD'))
5213 _git_amend_head(commit_desc.description, timestamp)
5214
5215 code, out = RunGitWithCode(
5216 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5217 print(out)
5218 if code == 0:
5219 break
5220 if IsFatalPushFailure(out):
5221 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005222 'user.email are correct and you have push access to the repo.\n'
5223 'Hint: run command below to diangose common Git/Gerrit credential '
5224 'problems:\n'
5225 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005226 break
5227 return code
5228
5229
5230def IsFatalPushFailure(push_stdout):
5231 """True if retrying push won't help."""
5232 return '(prohibited by Gerrit)' in push_stdout
5233
5234
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005235@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005236def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005237 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005238 parser.add_option('-b', dest='newbranch',
5239 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005240 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005241 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005242 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005243 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005244 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005245 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005246 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005247 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005248 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005249 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005250
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005251
5252 group = optparse.OptionGroup(
5253 parser,
5254 'Options for continuing work on the current issue uploaded from a '
5255 'different clone (e.g. different machine). Must be used independently '
5256 'from the other options. No issue number should be specified, and the '
5257 'branch must have an issue number associated with it')
5258 group.add_option('--reapply', action='store_true', dest='reapply',
5259 help='Reset the branch and reapply the issue.\n'
5260 'CAUTION: This will undo any local changes in this '
5261 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005262
5263 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005264 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005265 parser.add_option_group(group)
5266
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005267 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005268 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005269 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005270 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005271 auth_config = auth.extract_auth_config_from_options(options)
5272
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005273 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005274 if options.newbranch:
5275 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005276 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005277 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005278
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005279 cl = Changelist(auth_config=auth_config,
5280 codereview=options.forced_codereview)
5281 if not cl.GetIssue():
5282 parser.error('current branch must have an associated issue')
5283
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005284 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005285 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005286 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005287
5288 RunGit(['reset', '--hard', upstream])
5289 if options.pull:
5290 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005291
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005292 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5293 options.directory)
5294
5295 if len(args) != 1 or not args[0]:
5296 parser.error('Must specify issue number or url')
5297
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005298 target_issue_arg = ParseIssueNumberArgument(args[0],
5299 options.forced_codereview)
5300 if not target_issue_arg.valid:
5301 parser.error('invalid codereview url or CL id')
5302
5303 cl_kwargs = {
5304 'auth_config': auth_config,
5305 'codereview_host': target_issue_arg.hostname,
5306 'codereview': options.forced_codereview,
5307 }
5308 detected_codereview_from_url = False
5309 if target_issue_arg.codereview and not options.forced_codereview:
5310 detected_codereview_from_url = True
5311 cl_kwargs['codereview'] = target_issue_arg.codereview
5312 cl_kwargs['issue'] = target_issue_arg.issue
5313
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005314 # We don't want uncommitted changes mixed up with the patch.
5315 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005316 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005317
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005318 if options.newbranch:
5319 if options.force:
5320 RunGit(['branch', '-D', options.newbranch],
5321 stderr=subprocess2.PIPE, error_ok=True)
5322 RunGit(['new-branch', options.newbranch])
5323
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005324 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005325
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005326 if cl.IsGerrit():
5327 if options.reject:
5328 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005329 if options.directory:
5330 parser.error('--directory is not supported with Gerrit codereview.')
5331
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005332 if detected_codereview_from_url:
5333 print('canonical issue/change URL: %s (type: %s)\n' %
5334 (cl.GetIssueURL(), target_issue_arg.codereview))
5335
5336 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005337 options.nocommit, options.directory,
5338 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005339
5340
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005341def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005342 """Fetches the tree status and returns either 'open', 'closed',
5343 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005344 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005345 if url:
5346 status = urllib2.urlopen(url).read().lower()
5347 if status.find('closed') != -1 or status == '0':
5348 return 'closed'
5349 elif status.find('open') != -1 or status == '1':
5350 return 'open'
5351 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005352 return 'unset'
5353
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005355def GetTreeStatusReason():
5356 """Fetches the tree status from a json url and returns the message
5357 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005358 url = settings.GetTreeStatusUrl()
5359 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005360 connection = urllib2.urlopen(json_url)
5361 status = json.loads(connection.read())
5362 connection.close()
5363 return status['message']
5364
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005366def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005367 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005368 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005369 status = GetTreeStatus()
5370 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005371 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005372 return 2
5373
vapiera7fbd5a2016-06-16 09:17:49 -07005374 print('The tree is %s' % status)
5375 print()
5376 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005377 if status != 'open':
5378 return 1
5379 return 0
5380
5381
maruel@chromium.org15192402012-09-06 12:38:29 +00005382def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005383 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005384 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005385 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005386 '-b', '--bot', action='append',
5387 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5388 'times to specify multiple builders. ex: '
5389 '"-b win_rel -b win_layout". See '
5390 'the try server waterfall for the builders name and the tests '
5391 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005392 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005393 '-B', '--bucket', default='',
5394 help=('Buildbucket bucket to send the try requests.'))
5395 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005396 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005397 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005398 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005399 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005400 help='Revision to use for the try job; default: the revision will '
5401 'be determined by the try recipe that builder runs, which usually '
5402 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005403 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005404 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005405 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005406 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005407 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005408 '--category', default='git_cl_try', help='Specify custom build category.')
5409 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005410 '--project',
5411 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005412 'in recipe to determine to which repository or directory to '
5413 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005414 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005415 '-p', '--property', dest='properties', action='append', default=[],
5416 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005417 'key2=value2 etc. The value will be treated as '
5418 'json if decodable, or as string otherwise. '
5419 'NOTE: using this may make your try job not usable for CQ, '
5420 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005421 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005422 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5423 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005424 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005425 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005426 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005427 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005428 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005429 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005430
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005431 if options.master and options.master.startswith('luci.'):
5432 parser.error(
5433 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005434 # Make sure that all properties are prop=value pairs.
5435 bad_params = [x for x in options.properties if '=' not in x]
5436 if bad_params:
5437 parser.error('Got properties with missing "=": %s' % bad_params)
5438
maruel@chromium.org15192402012-09-06 12:38:29 +00005439 if args:
5440 parser.error('Unknown arguments: %s' % args)
5441
Koji Ishii31c14782018-01-08 17:17:33 +09005442 cl = Changelist(auth_config=auth_config, issue=options.issue,
5443 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005444 if not cl.GetIssue():
5445 parser.error('Need to upload first')
5446
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005447 if cl.IsGerrit():
5448 # HACK: warm up Gerrit change detail cache to save on RPCs.
5449 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5450
tandriie113dfd2016-10-11 10:20:12 -07005451 error_message = cl.CannotTriggerTryJobReason()
5452 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005453 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005454
borenet6c0efe62016-10-19 08:13:29 -07005455 if options.bucket and options.master:
5456 parser.error('Only one of --bucket and --master may be used.')
5457
qyearsley1fdfcb62016-10-24 13:22:03 -07005458 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005459
qyearsleydd49f942016-10-28 11:57:22 -07005460 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5461 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005462 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005463 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005464 print('git cl try with no bots now defaults to CQ dry run.')
5465 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5466 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005467
borenet6c0efe62016-10-19 08:13:29 -07005468 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005469 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005470 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005471 'of bot requires an initial job from a parent (usually a builder). '
5472 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005473 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005474 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005475
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005476 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005477 # TODO(tandrii): Checking local patchset against remote patchset is only
5478 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5479 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005480 print('Warning: Codereview server has newer patchsets (%s) than most '
5481 'recent upload from local checkout (%s). Did a previous upload '
5482 'fail?\n'
5483 'By default, git cl try uses the latest patchset from '
5484 'codereview, continuing to use patchset %s.\n' %
5485 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005486
tandrii568043b2016-10-11 07:49:18 -07005487 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005488 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005489 except BuildbucketResponseException as ex:
5490 print('ERROR: %s' % ex)
5491 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005492 return 0
5493
5494
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005495def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005496 """Prints info about try jobs associated with current CL."""
5497 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005498 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005499 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005500 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005501 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005502 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005503 '--color', action='store_true', default=setup_color.IS_TTY,
5504 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005505 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005506 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5507 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005508 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005509 '--json', help=('Path of JSON output file to write try job results to,'
5510 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005511 parser.add_option_group(group)
5512 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005513 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005514 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005515 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005516 if args:
5517 parser.error('Unrecognized args: %s' % ' '.join(args))
5518
5519 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005520 cl = Changelist(
5521 issue=options.issue, codereview=options.forced_codereview,
5522 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005523 if not cl.GetIssue():
5524 parser.error('Need to upload first')
5525
tandrii221ab252016-10-06 08:12:04 -07005526 patchset = options.patchset
5527 if not patchset:
5528 patchset = cl.GetMostRecentPatchset()
5529 if not patchset:
5530 parser.error('Codereview doesn\'t know about issue %s. '
5531 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005532 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005533 cl.GetIssue())
5534
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005535 # TODO(tandrii): Checking local patchset against remote patchset is only
5536 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5537 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005538 print('Warning: Codereview server has newer patchsets (%s) than most '
5539 'recent upload from local checkout (%s). Did a previous upload '
5540 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005541 'By default, git cl try-results uses the latest patchset from '
5542 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005543 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005544 try:
tandrii221ab252016-10-06 08:12:04 -07005545 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005546 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005547 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005548 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005549 if options.json:
5550 write_try_results_json(options.json, jobs)
5551 else:
5552 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005553 return 0
5554
5555
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005556@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005557def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005558 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005559 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005560 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005561 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005562
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005563 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005564 if args:
5565 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005566 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005567 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005568 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005569 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005570
5571 # Clear configured merge-base, if there is one.
5572 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005573 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005574 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005575 return 0
5576
5577
thestig@chromium.org00858c82013-12-02 23:08:03 +00005578def CMDweb(parser, args):
5579 """Opens the current CL in the web browser."""
5580 _, args = parser.parse_args(args)
5581 if args:
5582 parser.error('Unrecognized args: %s' % ' '.join(args))
5583
5584 issue_url = Changelist().GetIssueURL()
5585 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005586 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005587 return 1
5588
5589 webbrowser.open(issue_url)
5590 return 0
5591
5592
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005593def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005594 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005595 parser.add_option('-d', '--dry-run', action='store_true',
5596 help='trigger in dry run mode')
5597 parser.add_option('-c', '--clear', action='store_true',
5598 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005599 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005600 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005601 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005602 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005603 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005604 if args:
5605 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005606 if options.dry_run and options.clear:
5607 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5608
iannuccie53c9352016-08-17 14:40:40 -07005609 cl = Changelist(auth_config=auth_config, issue=options.issue,
5610 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005611 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005612 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005613 elif options.dry_run:
5614 state = _CQState.DRY_RUN
5615 else:
5616 state = _CQState.COMMIT
5617 if not cl.GetIssue():
5618 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005619 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005620 return 0
5621
5622
groby@chromium.org411034a2013-02-26 15:12:01 +00005623def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005624 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005625 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005626 auth.add_auth_options(parser)
5627 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005628 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005629 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005630 if args:
5631 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005632 cl = Changelist(auth_config=auth_config, issue=options.issue,
5633 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005634 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005635 if not cl.GetIssue():
5636 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005637 cl.CloseIssue()
5638 return 0
5639
5640
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005641def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005642 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005643 parser.add_option(
5644 '--stat',
5645 action='store_true',
5646 dest='stat',
5647 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005648 auth.add_auth_options(parser)
5649 options, args = parser.parse_args(args)
5650 auth_config = auth.extract_auth_config_from_options(options)
5651 if args:
5652 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005653
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005654 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005655 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005656 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005657 if not issue:
5658 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005659
Aaron Gablea718c3e2017-08-28 17:47:28 -07005660 base = cl._GitGetBranchConfigValue('last-upload-hash')
5661 if not base:
5662 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5663 if not base:
5664 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5665 revision_info = detail['revisions'][detail['current_revision']]
5666 fetch_info = revision_info['fetch']['http']
5667 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5668 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005669
Aaron Gablea718c3e2017-08-28 17:47:28 -07005670 cmd = ['git', 'diff']
5671 if options.stat:
5672 cmd.append('--stat')
5673 cmd.append(base)
5674 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005675
5676 return 0
5677
5678
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005679def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005680 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005681 parser.add_option(
5682 '--no-color',
5683 action='store_true',
5684 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005685 parser.add_option(
5686 '--batch',
5687 action='store_true',
5688 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005689 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005690 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005691 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005692
5693 author = RunGit(['config', 'user.email']).strip() or None
5694
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005695 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005696
5697 if args:
5698 if len(args) > 1:
5699 parser.error('Unknown args')
5700 base_branch = args[0]
5701 else:
5702 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005703 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005704
5705 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005706 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5707
5708 if options.batch:
5709 db = owners.Database(change.RepositoryRoot(), file, os.path)
5710 print('\n'.join(db.reviewers_for(affected_files, author)))
5711 return 0
5712
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005713 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005714 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005715 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005716 author,
5717 cl.GetReviewers(),
5718 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005719 disable_color=options.no_color,
5720 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005721
5722
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005723def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005724 """Generates a diff command."""
5725 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005726 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5727 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005728 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005729
5730 if args:
5731 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005732 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005733 diff_cmd.append(arg)
5734 else:
5735 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005736
5737 return diff_cmd
5738
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005739
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005740def MatchingFileType(file_name, extensions):
5741 """Returns true if the file name ends with one of the given extensions."""
5742 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005743
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005744
enne@chromium.org555cfe42014-01-29 18:21:39 +00005745@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005746def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005747 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005748 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005749 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005750 parser.add_option('--full', action='store_true',
5751 help='Reformat the full content of all touched files')
5752 parser.add_option('--dry-run', action='store_true',
5753 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005754 parser.add_option('--python', action='store_true',
5755 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005756 parser.add_option('--js', action='store_true',
5757 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005758 parser.add_option('--diff', action='store_true',
5759 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005760 parser.add_option('--presubmit', action='store_true',
5761 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005762 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005763
Daniel Chengc55eecf2016-12-30 03:11:02 -08005764 # Normalize any remaining args against the current path, so paths relative to
5765 # the current directory are still resolved as expected.
5766 args = [os.path.join(os.getcwd(), arg) for arg in args]
5767
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005768 # git diff generates paths against the root of the repository. Change
5769 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005770 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005771 if rel_base_path:
5772 os.chdir(rel_base_path)
5773
digit@chromium.org29e47272013-05-17 17:01:46 +00005774 # Grab the merge-base commit, i.e. the upstream commit of the current
5775 # branch when it was created or the last time it was rebased. This is
5776 # to cover the case where the user may have called "git fetch origin",
5777 # moving the origin branch to a newer commit, but hasn't rebased yet.
5778 upstream_commit = None
5779 cl = Changelist()
5780 upstream_branch = cl.GetUpstreamBranch()
5781 if upstream_branch:
5782 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5783 upstream_commit = upstream_commit.strip()
5784
5785 if not upstream_commit:
5786 DieWithError('Could not find base commit for this branch. '
5787 'Are you in detached state?')
5788
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005789 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5790 diff_output = RunGit(changed_files_cmd)
5791 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005792 # Filter out files deleted by this CL
5793 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005794
Christopher Lamc5ba6922017-01-24 11:19:14 +11005795 if opts.js:
5796 CLANG_EXTS.append('.js')
5797
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005798 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5799 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5800 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005801 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005802
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005803 top_dir = os.path.normpath(
5804 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5805
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005806 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5807 # formatted. This is used to block during the presubmit.
5808 return_value = 0
5809
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005810 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005811 # Locate the clang-format binary in the checkout
5812 try:
5813 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005814 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005815 DieWithError(e)
5816
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005817 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005818 cmd = [clang_format_tool]
5819 if not opts.dry_run and not opts.diff:
5820 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005821 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005822 if opts.diff:
5823 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005824 else:
5825 env = os.environ.copy()
5826 env['PATH'] = str(os.path.dirname(clang_format_tool))
5827 try:
5828 script = clang_format.FindClangFormatScriptInChromiumTree(
5829 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005830 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005831 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005832
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005833 cmd = [sys.executable, script, '-p0']
5834 if not opts.dry_run and not opts.diff:
5835 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005836
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005837 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5838 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005839
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005840 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5841 if opts.diff:
5842 sys.stdout.write(stdout)
5843 if opts.dry_run and len(stdout) > 0:
5844 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005845
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005846 # Similar code to above, but using yapf on .py files rather than clang-format
5847 # on C/C++ files
5848 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005849 yapf_tool = gclient_utils.FindExecutable('yapf')
5850 if yapf_tool is None:
5851 DieWithError('yapf not found in PATH')
5852
5853 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005854 if python_diff_files:
Nodir Turakulovaf43f402018-05-31 14:54:24 -07005855 if opts.dry_run or opts.diff:
5856 cmd = [yapf_tool, '--diff'] + python_diff_files
5857 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5858 if opts.diff:
5859 sys.stdout.write(stdout)
5860 elif len(stdout) > 0:
5861 return_value = 2
5862 else:
5863 RunCommand([yapf_tool, '-i'] + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005864 else:
5865 # TODO(sbc): yapf --lines mode still has some issues.
5866 # https://github.com/google/yapf/issues/154
5867 DieWithError('--python currently only works with --full')
5868
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005869 # Dart's formatter does not have the nice property of only operating on
5870 # modified chunks, so hard code full.
5871 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005872 try:
5873 command = [dart_format.FindDartFmtToolInChromiumTree()]
5874 if not opts.dry_run and not opts.diff:
5875 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005876 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005877
ppi@chromium.org6593d932016-03-03 15:41:15 +00005878 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005879 if opts.dry_run and stdout:
5880 return_value = 2
5881 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005882 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5883 'found in this checkout. Files in other languages are still '
5884 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005885
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005886 # Format GN build files. Always run on full build files for canonical form.
5887 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005888 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005889 if opts.dry_run or opts.diff:
5890 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005891 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005892 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5893 shell=sys.platform == 'win32',
5894 cwd=top_dir)
5895 if opts.dry_run and gn_ret == 2:
5896 return_value = 2 # Not formatted.
5897 elif opts.diff and gn_ret == 2:
5898 # TODO this should compute and print the actual diff.
5899 print("This change has GN build file diff for " + gn_diff_file)
5900 elif gn_ret != 0:
5901 # For non-dry run cases (and non-2 return values for dry-run), a
5902 # nonzero error code indicates a failure, probably because the file
5903 # doesn't parse.
5904 DieWithError("gn format failed on " + gn_diff_file +
5905 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005906
Ilya Shermane081cbe2017-08-15 17:51:04 -07005907 # Skip the metrics formatting from the global presubmit hook. These files have
5908 # a separate presubmit hook that issues an error if the files need formatting,
5909 # whereas the top-level presubmit script merely issues a warning. Formatting
5910 # these files is somewhat slow, so it's important not to duplicate the work.
5911 if not opts.presubmit:
5912 for xml_dir in GetDirtyMetricsDirs(diff_files):
5913 tool_dir = os.path.join(top_dir, xml_dir)
5914 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5915 if opts.dry_run or opts.diff:
5916 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005917 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005918 if opts.diff:
5919 sys.stdout.write(stdout)
5920 if opts.dry_run and stdout:
5921 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005922
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005923 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005924
Steven Holte2e664bf2017-04-21 13:10:47 -07005925def GetDirtyMetricsDirs(diff_files):
5926 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5927 metrics_xml_dirs = [
5928 os.path.join('tools', 'metrics', 'actions'),
5929 os.path.join('tools', 'metrics', 'histograms'),
5930 os.path.join('tools', 'metrics', 'rappor'),
5931 os.path.join('tools', 'metrics', 'ukm')]
5932 for xml_dir in metrics_xml_dirs:
5933 if any(file.startswith(xml_dir) for file in xml_diff_files):
5934 yield xml_dir
5935
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005936
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005937@subcommand.usage('<codereview url or issue id>')
5938def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005939 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005940 _, args = parser.parse_args(args)
5941
5942 if len(args) != 1:
5943 parser.print_help()
5944 return 1
5945
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005946 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005947 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005948 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005949
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005950 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005951
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005952 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005953 output = RunGit(['config', '--local', '--get-regexp',
5954 r'branch\..*\.%s' % issueprefix],
5955 error_ok=True)
5956 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005957 if issue == target_issue:
5958 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005959
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005960 branches = []
5961 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005962 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005963 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005964 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005965 return 1
5966 if len(branches) == 1:
5967 RunGit(['checkout', branches[0]])
5968 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005969 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005970 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005971 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005972 which = raw_input('Choose by index: ')
5973 try:
5974 RunGit(['checkout', branches[int(which)]])
5975 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005976 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005977 return 1
5978
5979 return 0
5980
5981
maruel@chromium.org29404b52014-09-08 22:58:00 +00005982def CMDlol(parser, args):
5983 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005984 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005985 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5986 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5987 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005988 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005989 return 0
5990
5991
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005992class OptionParser(optparse.OptionParser):
5993 """Creates the option parse and add --verbose support."""
5994 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005995 optparse.OptionParser.__init__(
5996 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005997 self.add_option(
5998 '-v', '--verbose', action='count', default=0,
5999 help='Use 2 times for more debugging info')
6000
6001 def parse_args(self, args=None, values=None):
6002 options, args = optparse.OptionParser.parse_args(self, args, values)
6003 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006004 logging.basicConfig(
6005 level=levels[min(options.verbose, len(levels) - 1)],
6006 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6007 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006008 return options, args
6009
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006010
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006011def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006012 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006013 print('\nYour python version %s is unsupported, please upgrade.\n' %
6014 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006015 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006016
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006017 # Reload settings.
6018 global settings
6019 settings = Settings()
6020
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006021 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006022 dispatcher = subcommand.CommandDispatcher(__name__)
6023 try:
6024 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006025 except auth.AuthenticationError as e:
6026 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006027 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006028 if e.code != 500:
6029 raise
6030 DieWithError(
6031 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6032 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006033 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006034
6035
6036if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006037 # These affect sys.stdout so do it outside of main() to simplify mocks in
6038 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006039 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006040 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006041 try:
6042 sys.exit(main(sys.argv[1:]))
6043 except KeyboardInterrupt:
6044 sys.stderr.write('interrupted\n')
6045 sys.exit(1)