blob: 40bca132cd059c080ac24994ddd034b6255badfe [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000058import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000059import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000061import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040063import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066import watchlists
67
tandrii7400cf02016-06-21 08:48:07 -070068__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069
tandrii9d2c7a32016-06-22 03:42:45 -070070COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070071DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
borenet6c0efe62016-10-19 08:13:29 -070083# Buildbucket master name prefix.
84MASTER_PREFIX = 'master.'
85
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000086# Shortcut since it quickly becomes redundant.
87Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000088
maruel@chromium.orgddd59412011-11-30 14:20:38 +000089# Initialized in main()
90settings = None
91
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010092# Used by tests/git_cl_test.py to add extra logging.
93# Inside the weirdly failing test, add this:
94# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070095# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096_IS_BEING_TESTED = False
97
maruel@chromium.orgddd59412011-11-30 14:20:38 +000098
Christopher Lamf732cd52017-01-24 12:40:11 +110099def DieWithError(message, change_desc=None):
100 if change_desc:
101 SaveDescriptionBackup(change_desc)
102
vapiera7fbd5a2016-06-16 09:17:49 -0700103 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 sys.exit(1)
105
106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def SaveDescriptionBackup(change_desc):
108 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
109 print('\nError after CL description prompt -- saving description to %s\n' %
110 backup_path)
111 backup_file = open(backup_path, 'w')
112 backup_file.write(change_desc.description)
113 backup_file.close()
114
115
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000116def GetNoGitPagerEnv():
117 env = os.environ.copy()
118 # 'cat' is a magical git string that disables pagers on all platforms.
119 env['GIT_PAGER'] = 'cat'
120 return env
121
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000122
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 'Command "%s" failed.\n%s' % (
131 ' '.join(args), error_message or e.stdout or ''))
132 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
135def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000137 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000140def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700142 if suppress_stderr:
143 stderr = subprocess2.VOID
144 else:
145 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000146 try:
tandrii5d48c322016-08-18 16:19:37 -0700147 (out, _), code = subprocess2.communicate(['git'] + args,
148 env=GetNoGitPagerEnv(),
149 stdout=subprocess2.PIPE,
150 stderr=stderr)
151 return code, out
152 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900153 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700154 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155
156
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000158 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159 return RunGitWithCode(args, suppress_stderr=True)[1]
160
161
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 return (version.startswith(prefix) and
166 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167
168
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000169def BranchExists(branch):
170 """Return True if specified branch exists."""
171 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
172 suppress_stderr=True)
173 return not code
174
175
tandrii2a16b952016-10-19 07:09:44 -0700176def time_sleep(seconds):
177 # Use this so that it can be mocked in tests without interfering with python
178 # system machinery.
179 import time # Local import to discourage others from importing time globally.
180 return time.sleep(seconds)
181
182
maruel@chromium.org90541732011-04-01 17:54:18 +0000183def ask_for_data(prompt):
184 try:
185 return raw_input(prompt)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100191def confirm_or_exit(prefix='', action='confirm'):
192 """Asks user to press enter to continue or press Ctrl+C to abort."""
193 if not prefix or prefix.endswith('\n'):
194 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100195 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100196 mid = ' Press'
197 elif prefix.endswith(' '):
198 mid = 'press'
199 else:
200 mid = ' press'
201 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
202
203
204def ask_for_explicit_yes(prompt):
205 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
206 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
207 while True:
208 if 'yes'.startswith(result):
209 return True
210 if 'no'.startswith(result):
211 return False
212 result = ask_for_data('Please, type yes or no: ').lower()
213
214
tandrii5d48c322016-08-18 16:19:37 -0700215def _git_branch_config_key(branch, key):
216 """Helper method to return Git config key for a branch."""
217 assert branch, 'branch name is required to set git config for it'
218 return 'branch.%s.%s' % (branch, key)
219
220
221def _git_get_branch_config_value(key, default=None, value_type=str,
222 branch=False):
223 """Returns git config value of given or current branch if any.
224
225 Returns default in all other cases.
226 """
227 assert value_type in (int, str, bool)
228 if branch is False: # Distinguishing default arg value from None.
229 branch = GetCurrentBranch()
230
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700232 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000233
tandrii5d48c322016-08-18 16:19:37 -0700234 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700235 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700237 # git config also has --int, but apparently git config suffers from integer
238 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append(_git_branch_config_key(branch, key))
240 code, out = RunGitWithCode(args)
241 if code == 0:
242 value = out.strip()
243 if value_type == int:
244 return int(value)
245 if value_type == bool:
246 return bool(value.lower() == 'true')
247 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 return default
249
250
tandrii5d48c322016-08-18 16:19:37 -0700251def _git_set_branch_config_value(key, value, branch=None, **kwargs):
252 """Sets the value or unsets if it's None of a git branch config.
253
254 Valid, though not necessarily existing, branch must be provided,
255 otherwise currently checked out branch is used.
256 """
257 if not branch:
258 branch = GetCurrentBranch()
259 assert branch, 'a branch name OR currently checked out branch is required'
260 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700261 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700262 if value is None:
263 args.append('--unset')
264 elif isinstance(value, bool):
265 args.append('--bool')
266 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700267 else:
tandrii33a46ff2016-08-23 05:53:40 -0700268 # git config also has --int, but apparently git config suffers from integer
269 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700270 value = str(value)
271 args.append(_git_branch_config_key(branch, key))
272 if value is not None:
273 args.append(value)
274 RunGit(args, **kwargs)
275
276
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100277def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700278 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100279
280 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
281 """
282 # Git also stores timezone offset, but it only affects visual display,
283 # actual point in time is defined by this timestamp only.
284 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
285
286
287def _git_amend_head(message, committer_timestamp):
288 """Amends commit with new message and desired committer_timestamp.
289
290 Sets committer timezone to UTC.
291 """
292 env = os.environ.copy()
293 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
294 return RunGit(['commit', '--amend', '-m', message], env=env)
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
354 if not content_json:
355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
357 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
358 content)
359 return content_json
360 if response.status < 500 or try_count >= 2:
361 raise httplib2.HttpLib2Error(content)
362
363 # status >= 500 means transient failures.
364 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700365 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 try_count += 1
367 assert False, 'unreachable'
368
369
qyearsley1fdfcb62016-10-24 13:22:03 -0700370def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700371 """Returns a dict mapping bucket names to builders and tests,
372 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373 """
qyearsleydd49f942016-10-28 11:57:22 -0700374 # If no bots are listed, we try to get a set of builders and tests based
375 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 if not options.bot:
377 change = changelist.GetChange(
378 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700379 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700380 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 change=change,
382 changed_files=change.LocalPaths(),
383 repository_root=settings.GetRoot(),
384 default_presubmit=None,
385 project=None,
386 verbose=options.verbose,
387 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700388 if masters is None:
389 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100390 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 if options.bucket:
393 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700394 if options.master:
395 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
qyearsleydd49f942016-10-28 11:57:22 -0700397 # If bots are listed but no master or bucket, then we need to find out
398 # the corresponding master for each bot.
399 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
400 if error_message:
401 option_parser.error(
402 'Tryserver master cannot be found because: %s\n'
403 'Please manually specify the tryserver master, e.g. '
404 '"-m tryserver.chromium.linux".' % error_message)
405 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
qyearsley123a4682016-10-26 09:12:17 -0700408def _get_bucket_map_for_builders(builders):
409 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 map_url = 'https://builders-map.appspot.com/'
411 try:
qyearsley123a4682016-10-26 09:12:17 -0700412 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 except urllib2.URLError as e:
414 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
415 (map_url, e))
416 except ValueError as e:
417 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700418 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 return None, 'Failed to build master map.'
420
qyearsley123a4682016-10-26 09:12:17 -0700421 bucket_map = {}
422 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800423 bucket = builders_map.get(builder, {}).get('bucket')
424 if bucket:
425 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700426 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700427
428
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800429def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 """Sends a request to Buildbucket to trigger try jobs for a changelist.
431
432 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700433 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700434 changelist: Changelist that the try jobs are associated with.
435 buckets: A nested dict mapping bucket names to builders to tests.
436 options: Command-line options.
437 """
tandriide281ae2016-10-12 06:02:30 -0700438 assert changelist.GetIssue(), 'CL must be uploaded first'
439 codereview_url = changelist.GetCodereviewServer()
440 assert codereview_url, 'CL must be uploaded first'
441 patchset = patchset or changelist.GetMostRecentPatchset()
442 assert patchset, 'CL must be uploaded first'
443
444 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700445 # Cache the buildbucket credentials under the codereview host key, so that
446 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700447 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000448 http = authenticator.authorize(httplib2.Http())
449 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700450
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000451 buildbucket_put_url = (
452 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000453 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700454 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
455 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
456 hostname=codereview_host,
457 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700459
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700460 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800461 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700462 if options.clobber:
463 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700464 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700465 if extra_properties:
466 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000467
468 batch_req_body = {'builds': []}
469 print_text = []
470 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700471 for bucket, builders_and_tests in sorted(buckets.iteritems()):
472 print_text.append('Bucket: %s' % bucket)
473 master = None
474 if bucket.startswith(MASTER_PREFIX):
475 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000476 for builder, tests in sorted(builders_and_tests.iteritems()):
477 print_text.append(' %s: %s' % (builder, tests))
478 parameters = {
479 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000480 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100481 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000482 'revision': options.revision,
483 }],
tandrii8c5a3532016-11-04 07:52:02 -0700484 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000486 if 'presubmit' in builder.lower():
487 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000488 if tests:
489 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700490
491 tags = [
492 'builder:%s' % builder,
493 'buildset:%s' % buildset,
494 'user_agent:git_cl_try',
495 ]
496 if master:
497 parameters['properties']['master'] = master
498 tags.append('master:%s' % master)
499
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 batch_req_body['builds'].append(
501 {
502 'bucket': bucket,
503 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700505 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 }
507 )
508
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700510 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 http,
512 buildbucket_put_url,
513 'PUT',
514 body=json.dumps(batch_req_body),
515 headers={'Content-Type': 'application/json'}
516 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000517 print_text.append('To see results here, run: git cl try-results')
518 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700519 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000520
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521
tandrii221ab252016-10-06 08:12:04 -0700522def fetch_try_jobs(auth_config, changelist, buildbucket_host,
523 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700524 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525
qyearsley53f48a12016-09-01 10:45:13 -0700526 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000527 """
tandrii221ab252016-10-06 08:12:04 -0700528 assert buildbucket_host
529 assert changelist.GetIssue(), 'CL must be uploaded first'
530 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
531 patchset = patchset or changelist.GetMostRecentPatchset()
532 assert patchset, 'CL must be uploaded first'
533
534 codereview_url = changelist.GetCodereviewServer()
535 codereview_host = urlparse.urlparse(codereview_url).hostname
536 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 if authenticator.has_cached_credentials():
538 http = authenticator.authorize(httplib2.Http())
539 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700540 print('Warning: Some results might be missing because %s' %
541 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700542 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 http = httplib2.Http()
544
545 http.force_exception_to_status_code = True
546
tandrii221ab252016-10-06 08:12:04 -0700547 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
548 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
549 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700551 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 params = {'tag': 'buildset:%s' % buildset}
553
554 builds = {}
555 while True:
556 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700557 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700559 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 for build in content.get('builds', []):
561 builds[build['id']] = build
562 if 'next_cursor' in content:
563 params['start_cursor'] = content['next_cursor']
564 else:
565 break
566 return builds
567
568
qyearsleyeab3c042016-08-24 09:18:28 -0700569def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 """Prints nicely result of fetch_try_jobs."""
571 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700572 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 return
574
575 # Make a copy, because we'll be modifying builds dictionary.
576 builds = builds.copy()
577 builder_names_cache = {}
578
579 def get_builder(b):
580 try:
581 return builder_names_cache[b['id']]
582 except KeyError:
583 try:
584 parameters = json.loads(b['parameters_json'])
585 name = parameters['builder_name']
586 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700587 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700588 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000589 name = None
590 builder_names_cache[b['id']] = name
591 return name
592
593 def get_bucket(b):
594 bucket = b['bucket']
595 if bucket.startswith('master.'):
596 return bucket[len('master.'):]
597 return bucket
598
599 if options.print_master:
600 name_fmt = '%%-%ds %%-%ds' % (
601 max(len(str(get_bucket(b))) for b in builds.itervalues()),
602 max(len(str(get_builder(b))) for b in builds.itervalues()))
603 def get_name(b):
604 return name_fmt % (get_bucket(b), get_builder(b))
605 else:
606 name_fmt = '%%-%ds' % (
607 max(len(str(get_builder(b))) for b in builds.itervalues()))
608 def get_name(b):
609 return name_fmt % get_builder(b)
610
611 def sort_key(b):
612 return b['status'], b.get('result'), get_name(b), b.get('url')
613
614 def pop(title, f, color=None, **kwargs):
615 """Pop matching builds from `builds` dict and print them."""
616
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000617 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 colorize = str
619 else:
620 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
621
622 result = []
623 for b in builds.values():
624 if all(b.get(k) == v for k, v in kwargs.iteritems()):
625 builds.pop(b['id'])
626 result.append(b)
627 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700628 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700630 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631
632 total = len(builds)
633 pop(status='COMPLETED', result='SUCCESS',
634 title='Successes:', color=Fore.GREEN,
635 f=lambda b: (get_name(b), b.get('url')))
636 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
637 title='Infra Failures:', color=Fore.MAGENTA,
638 f=lambda b: (get_name(b), b.get('url')))
639 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
640 title='Failures:', color=Fore.RED,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='CANCELED',
643 title='Canceled:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b),))
645 pop(status='COMPLETED', result='FAILURE',
646 failure_reason='INVALID_BUILD_DEFINITION',
647 title='Wrong master/builder name:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b),))
649 pop(status='COMPLETED', result='FAILURE',
650 title='Other failures:',
651 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
652 pop(status='COMPLETED',
653 title='Other finished:',
654 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
655 pop(status='STARTED',
656 title='Started:', color=Fore.YELLOW,
657 f=lambda b: (get_name(b), b.get('url')))
658 pop(status='SCHEDULED',
659 title='Scheduled:',
660 f=lambda b: (get_name(b), 'id=%s' % b['id']))
661 # The last section is just in case buildbucket API changes OR there is a bug.
662 pop(title='Other:',
663 f=lambda b: (get_name(b), 'id=%s' % b['id']))
664 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700665 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666
667
qyearsley53f48a12016-09-01 10:45:13 -0700668def write_try_results_json(output_file, builds):
669 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
670
671 The input |builds| dict is assumed to be generated by Buildbucket.
672 Buildbucket documentation: http://goo.gl/G0s101
673 """
674
675 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800676 """Extracts some of the information from one build dict."""
677 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700678 return {
679 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700680 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800681 'builder_name': parameters.get('builder_name'),
682 'created_ts': build.get('created_ts'),
683 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700684 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800685 'result': build.get('result'),
686 'status': build.get('status'),
687 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700688 'url': build.get('url'),
689 }
690
691 converted = []
692 for _, build in sorted(builds.items()):
693 converted.append(convert_build_dict(build))
694 write_json(output_file, converted)
695
696
Aaron Gable13101a62018-02-09 13:20:41 -0800697def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000698 """Prints statistics about the change to the user."""
699 # --no-ext-diff is broken in some versions of Git, so try to work around
700 # this by overriding the environment (but there is still a problem if the
701 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000702 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000703 if 'GIT_EXTERNAL_DIFF' in env:
704 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000705
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000706 try:
707 stdout = sys.stdout.fileno()
708 except AttributeError:
709 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000710 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800711 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000712 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000713
714
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000715class BuildbucketResponseException(Exception):
716 pass
717
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719class Settings(object):
720 def __init__(self):
721 self.default_server = None
722 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000723 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 self.tree_status_url = None
725 self.viewvc_url = None
726 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000727 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000728 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000729 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000730 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000731 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000732 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
734 def LazyUpdateIfNeeded(self):
735 """Updates the settings from a codereview.settings file, if available."""
736 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000737 # The only value that actually changes the behavior is
738 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000739 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000740 error_ok=True
741 ).strip().lower()
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000744 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 LoadCodereviewSettingsFromFile(cr_settings_file)
746 self.updated = True
747
748 def GetDefaultServerUrl(self, error_ok=False):
749 if not self.default_server:
750 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000751 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000752 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 if error_ok:
754 return self.default_server
755 if not self.default_server:
756 error_message = ('Could not find settings file. You must configure '
757 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000758 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 return self.default_server
761
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000762 @staticmethod
763 def GetRelativeRoot():
764 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000767 if self.root is None:
768 self.root = os.path.abspath(self.GetRelativeRoot())
769 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000771 def GetGitMirror(self, remote='origin'):
772 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000773 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000774 if not os.path.isdir(local_url):
775 return None
776 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
777 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100778 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100779 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000780 if mirror.exists():
781 return mirror
782 return None
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 def GetTreeStatusUrl(self, error_ok=False):
785 if not self.tree_status_url:
786 error_message = ('You must configure your tree status URL by running '
787 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 self.tree_status_url = self._GetRietveldConfig(
789 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return self.tree_status_url
791
792 def GetViewVCUrl(self):
793 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 return self.viewvc_url
796
rmistry@google.com90752582014-01-14 21:04:50 +0000797 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000798 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000799
rmistry@google.com78948ed2015-07-08 23:09:57 +0000800 def GetIsSkipDependencyUpload(self, branch_name):
801 """Returns true if specified branch should skip dep uploads."""
802 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
803 error_ok=True)
804
rmistry@google.com5626a922015-02-26 14:03:30 +0000805 def GetRunPostUploadHook(self):
806 run_post_upload_hook = self._GetRietveldConfig(
807 'run-post-upload-hook', error_ok=True)
808 return run_post_upload_hook == "True"
809
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000810 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000812
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000814 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000815
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700817 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000818 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700819 self.is_gerrit = (
820 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000821 return self.is_gerrit
822
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000823 def GetSquashGerritUploads(self):
824 """Return true if uploads to Gerrit should be squashed by default."""
825 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700826 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
827 if self.squash_gerrit_uploads is None:
828 # Default is squash now (http://crbug.com/611892#c23).
829 self.squash_gerrit_uploads = not (
830 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
831 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000832 return self.squash_gerrit_uploads
833
tandriia60502f2016-06-20 02:01:53 -0700834 def GetSquashGerritUploadsOverride(self):
835 """Return True or False if codereview.settings should be overridden.
836
837 Returns None if no override has been defined.
838 """
839 # See also http://crbug.com/611892#c23
840 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
841 error_ok=True).strip()
842 if result == 'true':
843 return True
844 if result == 'false':
845 return False
846 return None
847
tandrii@chromium.org28253532016-04-14 13:46:56 +0000848 def GetGerritSkipEnsureAuthenticated(self):
849 """Return True if EnsureAuthenticated should not be done for Gerrit
850 uploads."""
851 if self.gerrit_skip_ensure_authenticated is None:
852 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000853 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000854 error_ok=True).strip() == 'true')
855 return self.gerrit_skip_ensure_authenticated
856
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000857 def GetGitEditor(self):
858 """Return the editor specified in the git config, or None if none is."""
859 if self.git_editor is None:
860 self.git_editor = self._GetConfig('core.editor', error_ok=True)
861 return self.git_editor or None
862
thestig@chromium.org44202a22014-03-11 19:22:18 +0000863 def GetLintRegex(self):
864 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
865 DEFAULT_LINT_REGEX)
866
867 def GetLintIgnoreRegex(self):
868 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
869 DEFAULT_LINT_IGNORE_REGEX)
870
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000871 def GetProject(self):
872 if not self.project:
873 self.project = self._GetRietveldConfig('project', error_ok=True)
874 return self.project
875
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000876 def _GetRietveldConfig(self, param, **kwargs):
877 return self._GetConfig('rietveld.' + param, **kwargs)
878
rmistry@google.com78948ed2015-07-08 23:09:57 +0000879 def _GetBranchConfig(self, branch_name, param, **kwargs):
880 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 def _GetConfig(self, param, **kwargs):
883 self.LazyUpdateIfNeeded()
884 return RunGit(['config', param], **kwargs).strip()
885
886
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100887@contextlib.contextmanager
888def _get_gerrit_project_config_file(remote_url):
889 """Context manager to fetch and store Gerrit's project.config from
890 refs/meta/config branch and store it in temp file.
891
892 Provides a temporary filename or None if there was error.
893 """
894 error, _ = RunGitWithCode([
895 'fetch', remote_url,
896 '+refs/meta/config:refs/git_cl/meta/config'])
897 if error:
898 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700899 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100900 (remote_url, error))
901 yield None
902 return
903
904 error, project_config_data = RunGitWithCode(
905 ['show', 'refs/git_cl/meta/config:project.config'])
906 if error:
907 print('WARNING: project.config file not found')
908 yield None
909 return
910
911 with gclient_utils.temporary_directory() as tempdir:
912 project_config_file = os.path.join(tempdir, 'project.config')
913 gclient_utils.FileWrite(project_config_file, project_config_data)
914 yield project_config_file
915
916
917def _is_git_numberer_enabled(remote_url, remote_ref):
918 """Returns True if Git Numberer is enabled on this ref."""
919 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100920 KNOWN_PROJECTS_WHITELIST = [
921 'chromium/src',
922 'external/webrtc',
923 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100924 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200925 # For webrtc.googlesource.com/src.
926 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100927 ]
928
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100929 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
930 url_parts = urlparse.urlparse(remote_url)
931 project_name = url_parts.path.lstrip('/').rstrip('git./')
932 for known in KNOWN_PROJECTS_WHITELIST:
933 if project_name.endswith(known):
934 break
935 else:
936 # Early exit to avoid extra fetches for repos that aren't using Git
937 # Numberer.
938 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100939
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100940 with _get_gerrit_project_config_file(remote_url) as project_config_file:
941 if project_config_file is None:
942 # Failed to fetch project.config, which shouldn't happen on open source
943 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100944 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100945 def get_opts(x):
946 code, out = RunGitWithCode(
947 ['config', '-f', project_config_file, '--get-all',
948 'plugin.git-numberer.validate-%s-refglob' % x])
949 if code == 0:
950 return out.strip().splitlines()
951 return []
952 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100953
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100954 logging.info('validator config enabled %s disabled %s refglobs for '
955 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000956
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100957 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100958 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100959 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100960 return True
961 return False
962
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100963 if match_refglobs(disabled):
964 return False
965 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966
967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968def ShortBranchName(branch):
969 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970 return branch.replace('refs/heads/', '', 1)
971
972
973def GetCurrentBranchRef():
974 """Returns branch ref (e.g., refs/heads/master) or None."""
975 return RunGit(['symbolic-ref', 'HEAD'],
976 stderr=subprocess2.VOID, error_ok=True).strip() or None
977
978
979def GetCurrentBranch():
980 """Returns current branch or None.
981
982 For refs/heads/* branches, returns just last part. For others, full ref.
983 """
984 branchref = GetCurrentBranchRef()
985 if branchref:
986 return ShortBranchName(branchref)
987 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988
989
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000990class _CQState(object):
991 """Enum for states of CL with respect to Commit Queue."""
992 NONE = 'none'
993 DRY_RUN = 'dry_run'
994 COMMIT = 'commit'
995
996 ALL_STATES = [NONE, DRY_RUN, COMMIT]
997
998
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001000 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001001 self.issue = issue
1002 self.patchset = patchset
1003 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001004 assert codereview in (None, 'rietveld', 'gerrit')
1005 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001006
1007 @property
1008 def valid(self):
1009 return self.issue is not None
1010
1011
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001012def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001013 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1014 fail_result = _ParsedIssueNumberArgument()
1015
1016 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001017 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001018 if not arg.startswith('http'):
1019 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001020
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001021 url = gclient_utils.UpgradeToHttps(arg)
1022 try:
1023 parsed_url = urlparse.urlparse(url)
1024 except ValueError:
1025 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001026
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001027 if codereview is not None:
1028 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1029 return parsed or fail_result
1030
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001031 results = {}
1032 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1033 parsed = cls.ParseIssueURL(parsed_url)
1034 if parsed is not None:
1035 results[name] = parsed
1036
1037 if not results:
1038 return fail_result
1039 if len(results) == 1:
1040 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001041
1042 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1043 # This is likely Gerrit.
1044 return results['gerrit']
1045 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001046 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001047
1048
Aaron Gablea45ee112016-11-22 15:14:38 -08001049class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001050 def __init__(self, issue, url):
1051 self.issue = issue
1052 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001053 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001054
1055 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001056 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001057 self.issue, self.url)
1058
1059
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001060_CommentSummary = collections.namedtuple(
1061 '_CommentSummary', ['date', 'message', 'sender',
1062 # TODO(tandrii): these two aren't known in Gerrit.
1063 'approval', 'disapproval'])
1064
1065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001067 """Changelist works with one changelist in local branch.
1068
1069 Supports two codereview backends: Rietveld or Gerrit, selected at object
1070 creation.
1071
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001072 Notes:
1073 * Not safe for concurrent multi-{thread,process} use.
1074 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001075 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 """
1077
1078 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1079 """Create a new ChangeList instance.
1080
1081 If issue is given, the codereview must be given too.
1082
1083 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1084 Otherwise, it's decided based on current configuration of the local branch,
1085 with default being 'rietveld' for backwards compatibility.
1086 See _load_codereview_impl for more details.
1087
1088 **kwargs will be passed directly to codereview implementation.
1089 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001091 global settings
1092 if not settings:
1093 # Happens when git_cl.py is used as a utility library.
1094 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001095
1096 if issue:
1097 assert codereview, 'codereview must be known, if issue is known'
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.branchref = branchref
1100 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001101 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102 self.branch = ShortBranchName(self.branchref)
1103 else:
1104 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001106 self.lookedup_issue = False
1107 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 self.has_description = False
1109 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001110 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001112 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001113 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001118 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001119 assert self._codereview_impl
1120 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121
1122 def _load_codereview_impl(self, codereview=None, **kwargs):
1123 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001124 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1125 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1126 self._codereview = codereview
1127 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128 return
1129
1130 # Automatic selection based on issue number set for a current branch.
1131 # Rietveld takes precedence over Gerrit.
1132 assert not self.issue
1133 # Whether we find issue or not, we are doing the lookup.
1134 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001135 if self.GetBranch():
1136 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1137 issue = _git_get_branch_config_value(
1138 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1139 if issue:
1140 self._codereview = codereview
1141 self._codereview_impl = cls(self, **kwargs)
1142 self.issue = int(issue)
1143 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144
1145 # No issue is set for this branch, so decide based on repo-wide settings.
1146 return self._load_codereview_impl(
1147 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1148 **kwargs)
1149
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001150 def IsGerrit(self):
1151 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001152
1153 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001154 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001155
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001156 The return value is a string suitable for passing to git cl with the --cc
1157 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 """
1159 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001161 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001162 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1163 return self.cc
1164
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001165 def GetCCListWithoutDefault(self):
1166 """Return the users cc'd on this CL excluding default ones."""
1167 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001168 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001169 return self.cc
1170
Daniel Cheng7227d212017-11-17 08:12:37 -08001171 def ExtendCC(self, more_cc):
1172 """Extends the list of users to cc on this CL based on the changed files."""
1173 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
1175 def GetBranch(self):
1176 """Returns the short branch name, e.g. 'master'."""
1177 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001179 if not branchref:
1180 return None
1181 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 self.branch = ShortBranchName(self.branchref)
1183 return self.branch
1184
1185 def GetBranchRef(self):
1186 """Returns the full branch name, e.g. 'refs/heads/master'."""
1187 self.GetBranch() # Poke the lazy loader.
1188 return self.branchref
1189
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001190 def ClearBranch(self):
1191 """Clears cached branch data of this object."""
1192 self.branch = self.branchref = None
1193
tandrii5d48c322016-08-18 16:19:37 -07001194 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1195 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1196 kwargs['branch'] = self.GetBranch()
1197 return _git_get_branch_config_value(key, default, **kwargs)
1198
1199 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1200 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1201 assert self.GetBranch(), (
1202 'this CL must have an associated branch to %sset %s%s' %
1203 ('un' if value is None else '',
1204 key,
1205 '' if value is None else ' to %r' % value))
1206 kwargs['branch'] = self.GetBranch()
1207 return _git_set_branch_config_value(key, value, **kwargs)
1208
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001209 @staticmethod
1210 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001211 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 e.g. 'origin', 'refs/heads/master'
1213 """
1214 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001215 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001218 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001220 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1221 error_ok=True).strip()
1222 if upstream_branch:
1223 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001225 # Else, try to guess the origin remote.
1226 remote_branches = RunGit(['branch', '-r']).split()
1227 if 'origin/master' in remote_branches:
1228 # Fall back on origin/master if it exits.
1229 remote = 'origin'
1230 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001232 DieWithError(
1233 'Unable to determine default branch to diff against.\n'
1234 'Either pass complete "git diff"-style arguments, like\n'
1235 ' git cl upload origin/master\n'
1236 'or verify this branch is set up to track another \n'
1237 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238
1239 return remote, upstream_branch
1240
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001241 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001242 upstream_branch = self.GetUpstreamBranch()
1243 if not BranchExists(upstream_branch):
1244 DieWithError('The upstream for the current branch (%s) does not exist '
1245 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001246 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001247 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 def GetUpstreamBranch(self):
1250 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001253 upstream_branch = upstream_branch.replace('refs/heads/',
1254 'refs/remotes/%s/' % remote)
1255 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1256 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 self.upstream_branch = upstream_branch
1258 return self.upstream_branch
1259
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001260 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001261 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, branch = None, self.GetBranch()
1263 seen_branches = set()
1264 while branch not in seen_branches:
1265 seen_branches.add(branch)
1266 remote, branch = self.FetchUpstreamTuple(branch)
1267 branch = ShortBranchName(branch)
1268 if remote != '.' or branch.startswith('refs/remotes'):
1269 break
1270 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001271 remotes = RunGit(['remote'], error_ok=True).split()
1272 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001274 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001276 logging.warn('Could not determine which remote this change is '
1277 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001278 else:
1279 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001280 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 branch = 'HEAD'
1282 if branch.startswith('refs/remotes'):
1283 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001284 elif branch.startswith('refs/branch-heads/'):
1285 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 else:
1287 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001288 return self._remote
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 def GitSanityChecks(self, upstream_git_obj):
1291 """Checks git repo status and ensures diff is from local commits."""
1292
sbc@chromium.org79706062015-01-14 21:18:12 +00001293 if upstream_git_obj is None:
1294 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001295 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001296 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001297 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001298 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001299 return False
1300
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 # Verify the commit we're diffing against is in our current branch.
1302 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1303 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1304 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001305 print('ERROR: %s is not in the current branch. You may need to rebase '
1306 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001307 return False
1308
1309 # List the commits inside the diff, and verify they are all local.
1310 commits_in_diff = RunGit(
1311 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1312 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1313 remote_branch = remote_branch.strip()
1314 if code != 0:
1315 _, remote_branch = self.GetRemoteBranch()
1316
1317 commits_in_remote = RunGit(
1318 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1319
1320 common_commits = set(commits_in_diff) & set(commits_in_remote)
1321 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001322 print('ERROR: Your diff contains %d commits already in %s.\n'
1323 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1324 'the diff. If you are using a custom git flow, you can override'
1325 ' the reference used for this check with "git config '
1326 'gitcl.remotebranch <git-ref>".' % (
1327 len(common_commits), remote_branch, upstream_git_obj),
1328 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 return False
1330 return True
1331
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001332 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001333 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001334
1335 Returns None if it is not set.
1336 """
tandrii5d48c322016-08-18 16:19:37 -07001337 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 def GetRemoteUrl(self):
1340 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1341
1342 Returns None if there is no remote.
1343 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001345 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1346
1347 # If URL is pointing to a local directory, it is probably a git cache.
1348 if os.path.isdir(url):
1349 url = RunGit(['config', 'remote.%s.url' % remote],
1350 error_ok=True,
1351 cwd=url).strip()
1352 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001354 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001355 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001356 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001357 self.issue = self._GitGetBranchConfigValue(
1358 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 return self.issue
1361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 def GetIssueURL(self):
1363 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001364 issue = self.GetIssue()
1365 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001366 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001367 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001369 def GetDescription(self, pretty=False, force=False):
1370 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001372 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 self.has_description = True
1374 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001375 # Set width to 72 columns + 2 space indent.
1376 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001378 lines = self.description.splitlines()
1379 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 return self.description
1381
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001382 def GetDescriptionFooters(self):
1383 """Returns (non_footer_lines, footers) for the commit message.
1384
1385 Returns:
1386 non_footer_lines (list(str)) - Simple list of description lines without
1387 any footer. The lines do not contain newlines, nor does the list contain
1388 the empty line between the message and the footers.
1389 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1390 [("Change-Id", "Ideadbeef...."), ...]
1391 """
1392 raw_description = self.GetDescription()
1393 msg_lines, _, footers = git_footers.split_footers(raw_description)
1394 if footers:
1395 msg_lines = msg_lines[:len(msg_lines)-1]
1396 return msg_lines, footers
1397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001399 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001401 self.patchset = self._GitGetBranchConfigValue(
1402 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001403 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 return self.patchset
1405
1406 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001407 """Set this branch's patchset. If patchset=0, clears the patchset."""
1408 assert self.GetBranch()
1409 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001410 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001411 else:
1412 self.patchset = int(patchset)
1413 self._GitSetBranchConfigValue(
1414 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001416 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001417 """Set this branch's issue. If issue isn't given, clears the issue."""
1418 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001420 issue = int(issue)
1421 self._GitSetBranchConfigValue(
1422 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001423 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 codereview_server = self._codereview_impl.GetCodereviewServer()
1425 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001426 self._GitSetBranchConfigValue(
1427 self._codereview_impl.CodereviewServerConfigKey(),
1428 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 else:
tandrii5d48c322016-08-18 16:19:37 -07001430 # Reset all of these just to be clean.
1431 reset_suffixes = [
1432 'last-upload-hash',
1433 self._codereview_impl.IssueConfigKey(),
1434 self._codereview_impl.PatchsetConfigKey(),
1435 self._codereview_impl.CodereviewServerConfigKey(),
1436 ] + self._PostUnsetIssueProperties()
1437 for prop in reset_suffixes:
1438 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001439 msg = RunGit(['log', '-1', '--format=%B']).strip()
1440 if msg and git_footers.get_footer_change_id(msg):
1441 print('WARNING: The change patched into this branch has a Change-Id. '
1442 'Removing it.')
1443 RunGit(['commit', '--amend', '-m',
1444 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001446 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447
dnjba1b0f32016-09-02 12:37:42 -07001448 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 if not self.GitSanityChecks(upstream_branch):
1450 DieWithError('\nGit sanity check failure')
1451
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001452 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001453 if not root:
1454 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001456
1457 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001459 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001461 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001462 except subprocess2.CalledProcessError:
1463 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001464 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 'This branch probably doesn\'t exist anymore. To reset the\n'
1466 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001467 ' git branch --set-upstream-to origin/master %s\n'
1468 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470
maruel@chromium.org52424302012-08-29 15:14:30 +00001471 issue = self.GetIssue()
1472 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001473 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001474 description = self.GetDescription()
1475 else:
1476 # If the change was never uploaded, use the log messages of all commits
1477 # up to the branch point, as git cl upload will prefill the description
1478 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1480 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001481
1482 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001483 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001484 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001485 name,
1486 description,
1487 absroot,
1488 files,
1489 issue,
1490 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001491 author,
1492 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493
dsansomee2d6fd92016-09-08 00:10:47 -07001494 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001497 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001499 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1500 """Sets the description for this CL remotely.
1501
1502 You can get description_lines and footers with GetDescriptionFooters.
1503
1504 Args:
1505 description_lines (list(str)) - List of CL description lines without
1506 newline characters.
1507 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1508 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1509 `List-Of-Tokens`). It will be case-normalized so that each token is
1510 title-cased.
1511 """
1512 new_description = '\n'.join(description_lines)
1513 if footers:
1514 new_description += '\n'
1515 for k, v in footers:
1516 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1517 if not git_footers.FOOTER_PATTERN.match(foot):
1518 raise ValueError('Invalid footer %r' % foot)
1519 new_description += foot + '\n'
1520 self.UpdateDescription(new_description, force)
1521
Edward Lesmes8e282792018-04-03 18:50:29 -04001522 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001523 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1524 try:
1525 return presubmit_support.DoPresubmitChecks(change, committing,
1526 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1527 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001528 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1529 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001530 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001531 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001533 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1534 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001535 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1536 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001537 else:
1538 # Assume url.
1539 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1540 urlparse.urlparse(issue_arg))
1541 if not parsed_issue_arg or not parsed_issue_arg.valid:
1542 DieWithError('Failed to parse issue argument "%s". '
1543 'Must be an issue number or a valid URL.' % issue_arg)
1544 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001545 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001546
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001547 def CMDUpload(self, options, git_diff_args, orig_args):
1548 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001549 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001551 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001552 else:
1553 if self.GetBranch() is None:
1554 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1555
1556 # Default to diffing against common ancestor of upstream branch
1557 base_branch = self.GetCommonAncestorWithUpstream()
1558 git_diff_args = [base_branch, 'HEAD']
1559
Aaron Gablec4c40d12017-05-22 11:49:53 -07001560 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1561 if not self.IsGerrit() and not self.GetIssue():
1562 print('=====================================')
1563 print('NOTICE: Rietveld is being deprecated. '
1564 'You can upload changes to Gerrit with')
1565 print(' git cl upload --gerrit')
1566 print('or set Gerrit to be your default code review tool with')
1567 print(' git config gerrit.host true')
1568 print('=====================================')
1569
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001570 # Fast best-effort checks to abort before running potentially
1571 # expensive hooks if uploading is likely to fail anyway. Passing these
1572 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001573 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001574 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575
1576 # Apply watchlists on upload.
1577 change = self.GetChange(base_branch, None)
1578 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1579 files = [f.LocalPath() for f in change.AffectedFiles()]
1580 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001581 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001582
1583 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001584 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585 # Set the reviewer list now so that presubmit checks can access it.
1586 change_description = ChangeDescription(change.FullDescriptionText())
1587 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001588 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001589 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001590 change)
1591 change.SetDescriptionText(change_description.description)
1592 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001593 may_prompt=not options.force,
1594 verbose=options.verbose,
1595 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001596 if not hook_results.should_continue():
1597 return 1
1598 if not options.reviewers and hook_results.reviewers:
1599 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001600 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001602 # TODO(tandrii): Checking local patchset against remote patchset is only
1603 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1604 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001605 latest_patchset = self.GetMostRecentPatchset()
1606 local_patchset = self.GetPatchset()
1607 if (latest_patchset and local_patchset and
1608 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001609 print('The last upload made from this repository was patchset #%d but '
1610 'the most recent patchset on the server is #%d.'
1611 % (local_patchset, latest_patchset))
1612 print('Uploading will still work, but if you\'ve uploaded to this '
1613 'issue from another machine or branch the patch you\'re '
1614 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001615 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616
Aaron Gable13101a62018-02-09 13:20:41 -08001617 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001618 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001619 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001620 if self.IsGerrit():
1621 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1622 options.cq_dry_run);
1623 else:
1624 if options.use_commit_queue:
1625 self.SetCQState(_CQState.COMMIT)
1626 elif options.cq_dry_run:
1627 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001628
tandrii5d48c322016-08-18 16:19:37 -07001629 _git_set_branch_config_value('last-upload-hash',
1630 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631 # Run post upload hooks, if specified.
1632 if settings.GetRunPostUploadHook():
1633 presubmit_support.DoPostUploadExecuter(
1634 change,
1635 self,
1636 settings.GetRoot(),
1637 options.verbose,
1638 sys.stdout)
1639
1640 # Upload all dependencies if specified.
1641 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001642 print()
1643 print('--dependencies has been specified.')
1644 print('All dependent local branches will be re-uploaded.')
1645 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 # Remove the dependencies flag from args so that we do not end up in a
1647 # loop.
1648 orig_args.remove('--dependencies')
1649 ret = upload_branch_deps(self, orig_args)
1650 return ret
1651
Ravi Mistry31e7d562018-04-02 12:53:57 -04001652 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1653 """Sets labels on the change based on the provided flags.
1654
1655 Sets labels if issue is already uploaded and known, else returns without
1656 doing anything.
1657
1658 Args:
1659 enable_auto_submit: Sets Auto-Submit+1 on the change.
1660 use_commit_queue: Sets Commit-Queue+2 on the change.
1661 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1662 both use_commit_queue and cq_dry_run are true.
1663 """
1664 if not self.GetIssue():
1665 return
1666 try:
1667 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1668 cq_dry_run)
1669 return 0
1670 except KeyboardInterrupt:
1671 raise
1672 except:
1673 labels = []
1674 if enable_auto_submit:
1675 labels.append('Auto-Submit')
1676 if use_commit_queue or cq_dry_run:
1677 labels.append('Commit-Queue')
1678 print('WARNING: Failed to set label(s) on your change: %s\n'
1679 'Either:\n'
1680 ' * Your project does not have the above label(s),\n'
1681 ' * You don\'t have permission to set the above label(s),\n'
1682 ' * There\'s a bug in this code (see stack trace below).\n' %
1683 (', '.join(labels)))
1684 # Still raise exception so that stack trace is printed.
1685 raise
1686
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001687 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001688 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001689
1690 Issue must have been already uploaded and known.
1691 """
1692 assert new_state in _CQState.ALL_STATES
1693 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001694 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001695 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001696 return 0
1697 except KeyboardInterrupt:
1698 raise
1699 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001700 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001701 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001702 ' * Your project has no CQ,\n'
1703 ' * You don\'t have permission to change the CQ state,\n'
1704 ' * There\'s a bug in this code (see stack trace below).\n'
1705 'Consider specifying which bots to trigger manually or asking your '
1706 'project owners for permissions or contacting Chrome Infra at:\n'
1707 'https://www.chromium.org/infra\n\n' %
1708 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001709 # Still raise exception so that stack trace is printed.
1710 raise
1711
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712 # Forward methods to codereview specific implementation.
1713
Aaron Gable636b13f2017-07-14 10:42:48 -07001714 def AddComment(self, message, publish=None):
1715 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001716
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001717 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001718 """Returns list of _CommentSummary for each comment.
1719
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001720 args:
1721 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001722 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001723 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001724
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 def CloseIssue(self):
1726 return self._codereview_impl.CloseIssue()
1727
1728 def GetStatus(self):
1729 return self._codereview_impl.GetStatus()
1730
1731 def GetCodereviewServer(self):
1732 return self._codereview_impl.GetCodereviewServer()
1733
tandriide281ae2016-10-12 06:02:30 -07001734 def GetIssueOwner(self):
1735 """Get owner from codereview, which may differ from this checkout."""
1736 return self._codereview_impl.GetIssueOwner()
1737
Edward Lemur707d70b2018-02-07 00:50:14 +01001738 def GetReviewers(self):
1739 return self._codereview_impl.GetReviewers()
1740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetMostRecentPatchset(self):
1742 return self._codereview_impl.GetMostRecentPatchset()
1743
tandriide281ae2016-10-12 06:02:30 -07001744 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001745 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001746 return self._codereview_impl.CannotTriggerTryJobReason()
1747
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001748 def GetTryJobProperties(self, patchset=None):
1749 """Returns dictionary of properties to launch try job."""
1750 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001751
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001752 def __getattr__(self, attr):
1753 # This is because lots of untested code accesses Rietveld-specific stuff
1754 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001755 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001756 # Note that child method defines __getattr__ as well, and forwards it here,
1757 # because _RietveldChangelistImpl is not cleaned up yet, and given
1758 # deprecation of Rietveld, it should probably be just removed.
1759 # Until that time, avoid infinite recursion by bypassing __getattr__
1760 # of implementation class.
1761 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001762
1763
1764class _ChangelistCodereviewBase(object):
1765 """Abstract base class encapsulating codereview specifics of a changelist."""
1766 def __init__(self, changelist):
1767 self._changelist = changelist # instance of Changelist
1768
1769 def __getattr__(self, attr):
1770 # Forward methods to changelist.
1771 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1772 # _RietveldChangelistImpl to avoid this hack?
1773 return getattr(self._changelist, attr)
1774
1775 def GetStatus(self):
1776 """Apply a rough heuristic to give a simple summary of an issue's review
1777 or CQ status, assuming adherence to a common workflow.
1778
1779 Returns None if no issue for this branch, or specific string keywords.
1780 """
1781 raise NotImplementedError()
1782
1783 def GetCodereviewServer(self):
1784 """Returns server URL without end slash, like "https://codereview.com"."""
1785 raise NotImplementedError()
1786
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001787 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 """Fetches and returns description from the codereview server."""
1789 raise NotImplementedError()
1790
tandrii5d48c322016-08-18 16:19:37 -07001791 @classmethod
1792 def IssueConfigKey(cls):
1793 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 raise NotImplementedError()
1795
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001796 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001797 def PatchsetConfigKey(cls):
1798 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799 raise NotImplementedError()
1800
tandrii5d48c322016-08-18 16:19:37 -07001801 @classmethod
1802 def CodereviewServerConfigKey(cls):
1803 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 raise NotImplementedError()
1805
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001806 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001807 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001808 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001809
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001810 def GetGerritObjForPresubmit(self):
1811 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1812 return None
1813
dsansomee2d6fd92016-09-08 00:10:47 -07001814 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001815 """Update the description on codereview site."""
1816 raise NotImplementedError()
1817
Aaron Gable636b13f2017-07-14 10:42:48 -07001818 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001819 """Posts a comment to the codereview site."""
1820 raise NotImplementedError()
1821
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001822 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001823 raise NotImplementedError()
1824
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 def CloseIssue(self):
1826 """Closes the issue."""
1827 raise NotImplementedError()
1828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829 def GetMostRecentPatchset(self):
1830 """Returns the most recent patchset number from the codereview site."""
1831 raise NotImplementedError()
1832
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001833 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001834 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001835 """Fetches and applies the issue.
1836
1837 Arguments:
1838 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1839 reject: if True, reject the failed patch instead of switching to 3-way
1840 merge. Rietveld only.
1841 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1842 only.
1843 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001844 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001845 """
1846 raise NotImplementedError()
1847
1848 @staticmethod
1849 def ParseIssueURL(parsed_url):
1850 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1851 failed."""
1852 raise NotImplementedError()
1853
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001854 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001855 """Best effort check that user is authenticated with codereview server.
1856
1857 Arguments:
1858 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001859 refresh: whether to attempt to refresh credentials. Ignored if not
1860 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001861 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862 raise NotImplementedError()
1863
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001864 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001865 """Best effort check that uploading isn't supposed to fail for predictable
1866 reasons.
1867
1868 This method should raise informative exception if uploading shouldn't
1869 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001870
1871 Arguments:
1872 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001873 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001874 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001875
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001876 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001877 """Uploads a change to codereview."""
1878 raise NotImplementedError()
1879
Ravi Mistry31e7d562018-04-02 12:53:57 -04001880 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1881 """Sets labels on the change based on the provided flags.
1882
1883 Issue must have been already uploaded and known.
1884 """
1885 raise NotImplementedError()
1886
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001887 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001888 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001889
1890 Issue must have been already uploaded and known.
1891 """
1892 raise NotImplementedError()
1893
tandriie113dfd2016-10-11 10:20:12 -07001894 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001895 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001896 raise NotImplementedError()
1897
tandriide281ae2016-10-12 06:02:30 -07001898 def GetIssueOwner(self):
1899 raise NotImplementedError()
1900
Edward Lemur707d70b2018-02-07 00:50:14 +01001901 def GetReviewers(self):
1902 raise NotImplementedError()
1903
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001904 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001905 raise NotImplementedError()
1906
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001907
1908class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001909
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001910 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001911 super(_RietveldChangelistImpl, self).__init__(changelist)
1912 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001913 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001914 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001915
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001916 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001917 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918 self._props = None
1919 self._rpc_server = None
1920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001921 def GetCodereviewServer(self):
1922 if not self._rietveld_server:
1923 # If we're on a branch then get the server potentially associated
1924 # with that branch.
1925 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001926 self._rietveld_server = gclient_utils.UpgradeToHttps(
1927 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001928 if not self._rietveld_server:
1929 self._rietveld_server = settings.GetDefaultServerUrl()
1930 return self._rietveld_server
1931
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001932 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001933 """Best effort check that user is authenticated with Rietveld server."""
1934 if self._auth_config.use_oauth2:
1935 authenticator = auth.get_authenticator_for_host(
1936 self.GetCodereviewServer(), self._auth_config)
1937 if not authenticator.has_cached_credentials():
1938 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001939 if refresh:
1940 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001941
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001942 def EnsureCanUploadPatchset(self, force):
1943 # No checks for Rietveld because we are deprecating Rietveld.
1944 pass
1945
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001946 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 issue = self.GetIssue()
1948 assert issue
1949 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001950 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951 except urllib2.HTTPError as e:
1952 if e.code == 404:
1953 DieWithError(
1954 ('\nWhile fetching the description for issue %d, received a '
1955 '404 (not found)\n'
1956 'error. It is likely that you deleted this '
1957 'issue on the server. If this is the\n'
1958 'case, please run\n\n'
1959 ' git cl issue 0\n\n'
1960 'to clear the association with the deleted issue. Then run '
1961 'this command again.') % issue)
1962 else:
1963 DieWithError(
1964 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1965 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001966 print('Warning: Failed to retrieve CL description due to network '
1967 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001968 return ''
1969
1970 def GetMostRecentPatchset(self):
1971 return self.GetIssueProperties()['patchsets'][-1]
1972
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 def GetIssueProperties(self):
1974 if self._props is None:
1975 issue = self.GetIssue()
1976 if not issue:
1977 self._props = {}
1978 else:
1979 self._props = self.RpcServer().get_issue_properties(issue, True)
1980 return self._props
1981
tandriie113dfd2016-10-11 10:20:12 -07001982 def CannotTriggerTryJobReason(self):
1983 props = self.GetIssueProperties()
1984 if not props:
1985 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1986 if props.get('closed'):
1987 return 'CL %s is closed' % self.GetIssue()
1988 if props.get('private'):
1989 return 'CL %s is private' % self.GetIssue()
1990 return None
1991
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001992 def GetTryJobProperties(self, patchset=None):
1993 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001994 project = (self.GetIssueProperties() or {}).get('project')
1995 return {
1996 'issue': self.GetIssue(),
1997 'patch_project': project,
1998 'patch_storage': 'rietveld',
1999 'patchset': patchset or self.GetPatchset(),
2000 'rietveld': self.GetCodereviewServer(),
2001 }
2002
tandriide281ae2016-10-12 06:02:30 -07002003 def GetIssueOwner(self):
2004 return (self.GetIssueProperties() or {}).get('owner_email')
2005
Edward Lemur707d70b2018-02-07 00:50:14 +01002006 def GetReviewers(self):
2007 return (self.GetIssueProperties() or {}).get('reviewers')
2008
Aaron Gable636b13f2017-07-14 10:42:48 -07002009 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002010 return self.RpcServer().add_comment(self.GetIssue(), message)
2011
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002012 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002013 summary = []
2014 for message in self.GetIssueProperties().get('messages', []):
2015 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2016 summary.append(_CommentSummary(
2017 date=date,
2018 disapproval=bool(message['disapproval']),
2019 approval=bool(message['approval']),
2020 sender=message['sender'],
2021 message=message['text'],
2022 ))
2023 return summary
2024
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002025 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002026 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002027 or CQ status, assuming adherence to a common workflow.
2028
2029 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002030 * 'error' - error from review tool (including deleted issues)
2031 * 'unsent' - not sent for review
2032 * 'waiting' - waiting for review
2033 * 'reply' - waiting for owner to reply to review
2034 * 'not lgtm' - Code-Review label has been set negatively
2035 * 'lgtm' - LGTM from at least one approved reviewer
2036 * 'commit' - in the commit queue
2037 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002038 """
2039 if not self.GetIssue():
2040 return None
2041
2042 try:
2043 props = self.GetIssueProperties()
2044 except urllib2.HTTPError:
2045 return 'error'
2046
2047 if props.get('closed'):
2048 # Issue is closed.
2049 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002050 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002051 # Issue is in the commit queue.
2052 return 'commit'
2053
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002054 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002055 if not messages:
2056 # No message was sent.
2057 return 'unsent'
2058
2059 if get_approving_reviewers(props):
2060 return 'lgtm'
2061 elif get_approving_reviewers(props, disapproval=True):
2062 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002063
tandrii9d2c7a32016-06-22 03:42:45 -07002064 # Skip CQ messages that don't require owner's action.
2065 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2066 if 'Dry run:' in messages[-1]['text']:
2067 messages.pop()
2068 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2069 # This message always follows prior messages from CQ,
2070 # so skip this too.
2071 messages.pop()
2072 else:
2073 # This is probably a CQ messages warranting user attention.
2074 break
2075
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002076 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002077 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002078 return 'reply'
2079 return 'waiting'
2080
dsansomee2d6fd92016-09-08 00:10:47 -07002081 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002082 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002083
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002085 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002087 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002088 return self.SetFlags({flag: value})
2089
2090 def SetFlags(self, flags):
2091 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002092 """
phajdan.jr68598232016-08-10 03:28:28 -07002093 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002094 try:
tandrii4b233bd2016-07-06 03:50:29 -07002095 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002096 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002097 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002098 if e.code == 404:
2099 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2100 if e.code == 403:
2101 DieWithError(
2102 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002103 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002106 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107 """Returns an upload.RpcServer() to access this review's rietveld instance.
2108 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002109 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002110 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002112 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002113 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002115 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002116 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002117 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118
tandrii5d48c322016-08-18 16:19:37 -07002119 @classmethod
2120 def PatchsetConfigKey(cls):
2121 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122
tandrii5d48c322016-08-18 16:19:37 -07002123 @classmethod
2124 def CodereviewServerConfigKey(cls):
2125 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002126
Ravi Mistry31e7d562018-04-02 12:53:57 -04002127 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2128 raise NotImplementedError()
2129
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002130 def SetCQState(self, new_state):
2131 props = self.GetIssueProperties()
2132 if props.get('private'):
2133 DieWithError('Cannot set-commit on private issue')
2134
2135 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002136 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002137 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002138 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002139 else:
tandrii4b233bd2016-07-06 03:50:29 -07002140 assert new_state == _CQState.DRY_RUN
2141 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002142
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002143 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002144 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002145 # PatchIssue should never be called with a dirty tree. It is up to the
2146 # caller to check this, but just in case we assert here since the
2147 # consequences of the caller not checking this could be dire.
2148 assert(not git_common.is_dirty_git_tree('apply'))
2149 assert(parsed_issue_arg.valid)
2150 self._changelist.issue = parsed_issue_arg.issue
2151 if parsed_issue_arg.hostname:
2152 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2153
skobes6468b902016-10-24 08:45:10 -07002154 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2155 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2156 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157 try:
skobes6468b902016-10-24 08:45:10 -07002158 scm_obj.apply_patch(patchset_object)
2159 except Exception as e:
2160 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 return 1
2162
2163 # If we had an issue, commit the current state and register the issue.
2164 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002165 self.SetIssue(self.GetIssue())
2166 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2168 'patch from issue %(i)s at patchset '
2169 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2170 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002171 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002172 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002173 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002174 return 0
2175
2176 @staticmethod
2177 def ParseIssueURL(parsed_url):
2178 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2179 return None
wychen3c1c1722016-08-04 11:46:36 -07002180 # Rietveld patch: https://domain/<number>/#ps<patchset>
2181 match = re.match(r'/(\d+)/$', parsed_url.path)
2182 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2183 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002184 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002185 issue=int(match.group(1)),
2186 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002187 hostname=parsed_url.netloc,
2188 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002189 # Typical url: https://domain/<issue_number>[/[other]]
2190 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2191 if match:
skobes6468b902016-10-24 08:45:10 -07002192 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002193 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002194 hostname=parsed_url.netloc,
2195 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002196 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2197 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2198 if match:
skobes6468b902016-10-24 08:45:10 -07002199 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002200 issue=int(match.group(1)),
2201 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002202 hostname=parsed_url.netloc,
2203 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002204 return None
2205
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002206 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002207 """Upload the patch to Rietveld."""
2208 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2209 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002210 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2211 if options.emulate_svn_auto_props:
2212 upload_args.append('--emulate_svn_auto_props')
2213
2214 change_desc = None
2215
2216 if options.email is not None:
2217 upload_args.extend(['--email', options.email])
2218
2219 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002220 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002221 upload_args.extend(['--title', options.title])
2222 if options.message:
2223 upload_args.extend(['--message', options.message])
2224 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002225 print('This branch is associated with issue %s. '
2226 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 else:
nodirca166002016-06-27 10:59:51 -07002228 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002229 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002230 if options.message:
2231 message = options.message
2232 else:
2233 message = CreateDescriptionFromLog(args)
2234 if options.title:
2235 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002236 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002237 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002238 change_desc.update_reviewers(options.reviewers, options.tbrs,
2239 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002241 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242
2243 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002244 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002245 return 1
2246
2247 upload_args.extend(['--message', change_desc.description])
2248 if change_desc.get_reviewers():
2249 upload_args.append('--reviewers=%s' % ','.join(
2250 change_desc.get_reviewers()))
2251 if options.send_mail:
2252 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002253 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002254 upload_args.append('--send_mail')
2255
2256 # We check this before applying rietveld.private assuming that in
2257 # rietveld.cc only addresses which we can send private CLs to are listed
2258 # if rietveld.private is set, and so we should ignore rietveld.cc only
2259 # when --private is specified explicitly on the command line.
2260 if options.private:
2261 logging.warn('rietveld.cc is ignored since private flag is specified. '
2262 'You need to review and add them manually if necessary.')
2263 cc = self.GetCCListWithoutDefault()
2264 else:
2265 cc = self.GetCCList()
2266 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002267 if change_desc.get_cced():
2268 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002269 if cc:
2270 upload_args.extend(['--cc', cc])
2271
2272 if options.private or settings.GetDefaultPrivateFlag() == "True":
2273 upload_args.append('--private')
2274
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 # Include the upstream repo's URL in the change -- this is useful for
2276 # projects that have their source spread across multiple repos.
2277 remote_url = self.GetGitBaseUrlFromConfig()
2278 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002279 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2280 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2281 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002284 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 if target_ref:
2286 upload_args.extend(['--target_ref', target_ref])
2287
2288 # Look for dependent patchsets. See crbug.com/480453 for more details.
2289 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2290 upstream_branch = ShortBranchName(upstream_branch)
2291 if remote is '.':
2292 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002293 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002294 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002295 print()
2296 print('Skipping dependency patchset upload because git config '
2297 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2298 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 else:
2300 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002301 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 auth_config=auth_config)
2303 branch_cl_issue_url = branch_cl.GetIssueURL()
2304 branch_cl_issue = branch_cl.GetIssue()
2305 branch_cl_patchset = branch_cl.GetPatchset()
2306 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2307 upload_args.extend(
2308 ['--depends_on_patchset', '%s:%s' % (
2309 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002310 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 '\n'
2312 'The current branch (%s) is tracking a local branch (%s) with '
2313 'an associated CL.\n'
2314 'Adding %s/#ps%s as a dependency patchset.\n'
2315 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2316 branch_cl_patchset))
2317
2318 project = settings.GetProject()
2319 if project:
2320 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002321 else:
2322 print()
2323 print('WARNING: Uploading without a project specified. Please ensure '
2324 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2325 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002326
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002327 try:
2328 upload_args = ['upload'] + upload_args + args
2329 logging.info('upload.RealMain(%s)', upload_args)
2330 issue, patchset = upload.RealMain(upload_args)
2331 issue = int(issue)
2332 patchset = int(patchset)
2333 except KeyboardInterrupt:
2334 sys.exit(1)
2335 except:
2336 # If we got an exception after the user typed a description for their
2337 # change, back up the description before re-raising.
2338 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002339 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002340 raise
2341
2342 if not self.GetIssue():
2343 self.SetIssue(issue)
2344 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002345 return 0
2346
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002347
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002348class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002349 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002350 # auth_config is Rietveld thing, kept here to preserve interface only.
2351 super(_GerritChangelistImpl, self).__init__(changelist)
2352 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002353 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002354 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002355 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002356 # Map from change number (issue) to its detail cache.
2357 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002358
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002359 if codereview_host is not None:
2360 assert not codereview_host.startswith('https://'), codereview_host
2361 self._gerrit_host = codereview_host
2362 self._gerrit_server = 'https://%s' % codereview_host
2363
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002364 def _GetGerritHost(self):
2365 # Lazy load of configs.
2366 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002367 if self._gerrit_host and '.' not in self._gerrit_host:
2368 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2369 # This happens for internal stuff http://crbug.com/614312.
2370 parsed = urlparse.urlparse(self.GetRemoteUrl())
2371 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002372 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002373 ' Your current remote is: %s' % self.GetRemoteUrl())
2374 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2375 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002376 return self._gerrit_host
2377
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002378 def _GetGitHost(self):
2379 """Returns git host to be used when uploading change to Gerrit."""
2380 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2381
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002382 def GetCodereviewServer(self):
2383 if not self._gerrit_server:
2384 # If we're on a branch then get the server potentially associated
2385 # with that branch.
2386 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002387 self._gerrit_server = self._GitGetBranchConfigValue(
2388 self.CodereviewServerConfigKey())
2389 if self._gerrit_server:
2390 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002391 if not self._gerrit_server:
2392 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2393 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002394 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002395 parts[0] = parts[0] + '-review'
2396 self._gerrit_host = '.'.join(parts)
2397 self._gerrit_server = 'https://%s' % self._gerrit_host
2398 return self._gerrit_server
2399
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002400 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002401 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002402 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002403
tandrii5d48c322016-08-18 16:19:37 -07002404 @classmethod
2405 def PatchsetConfigKey(cls):
2406 return 'gerritpatchset'
2407
2408 @classmethod
2409 def CodereviewServerConfigKey(cls):
2410 return 'gerritserver'
2411
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002412 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002413 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002414 if settings.GetGerritSkipEnsureAuthenticated():
2415 # For projects with unusual authentication schemes.
2416 # See http://crbug.com/603378.
2417 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002418 # Lazy-loader to identify Gerrit and Git hosts.
2419 if gerrit_util.GceAuthenticator.is_gce():
2420 return
2421 self.GetCodereviewServer()
2422 git_host = self._GetGitHost()
2423 assert self._gerrit_server and self._gerrit_host
2424 cookie_auth = gerrit_util.CookiesAuthenticator()
2425
2426 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2427 git_auth = cookie_auth.get_auth_header(git_host)
2428 if gerrit_auth and git_auth:
2429 if gerrit_auth == git_auth:
2430 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002431 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002432 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002433 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002434 ' %s\n'
2435 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002436 ' Consider running the following command:\n'
2437 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002438 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002439 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002440 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002441 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002442 cookie_auth.get_new_password_message(git_host)))
2443 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002444 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002445 return
2446 else:
2447 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002448 ([] if gerrit_auth else [self._gerrit_host]) +
2449 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002450 DieWithError('Credentials for the following hosts are required:\n'
2451 ' %s\n'
2452 'These are read from %s (or legacy %s)\n'
2453 '%s' % (
2454 '\n '.join(missing),
2455 cookie_auth.get_gitcookies_path(),
2456 cookie_auth.get_netrc_path(),
2457 cookie_auth.get_new_password_message(git_host)))
2458
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002459 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002460 if not self.GetIssue():
2461 return
2462
2463 # Warm change details cache now to avoid RPCs later, reducing latency for
2464 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002465 self._GetChangeDetail(
2466 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002467
2468 status = self._GetChangeDetail()['status']
2469 if status in ('MERGED', 'ABANDONED'):
2470 DieWithError('Change %s has been %s, new uploads are not allowed' %
2471 (self.GetIssueURL(),
2472 'submitted' if status == 'MERGED' else 'abandoned'))
2473
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002474 if gerrit_util.GceAuthenticator.is_gce():
2475 return
2476 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2477 self._GetGerritHost())
2478 if self.GetIssueOwner() == cookies_user:
2479 return
2480 logging.debug('change %s owner is %s, cookies user is %s',
2481 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002482 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002483 # so ask what Gerrit thinks of this user.
2484 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2485 if details['email'] == self.GetIssueOwner():
2486 return
2487 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002488 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002489 'as %s.\n'
2490 'Uploading may fail due to lack of permissions.' %
2491 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2492 confirm_or_exit(action='upload')
2493
2494
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002495 def _PostUnsetIssueProperties(self):
2496 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002497 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002498
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002499 def GetGerritObjForPresubmit(self):
2500 return presubmit_support.GerritAccessor(self._GetGerritHost())
2501
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002502 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002503 """Apply a rough heuristic to give a simple summary of an issue's review
2504 or CQ status, assuming adherence to a common workflow.
2505
2506 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002507 * 'error' - error from review tool (including deleted issues)
2508 * 'unsent' - no reviewers added
2509 * 'waiting' - waiting for review
2510 * 'reply' - waiting for uploader to reply to review
2511 * 'lgtm' - Code-Review label has been set
2512 * 'commit' - in the commit queue
2513 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002514 """
2515 if not self.GetIssue():
2516 return None
2517
2518 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002519 data = self._GetChangeDetail([
2520 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002521 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002522 return 'error'
2523
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002524 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002525 return 'closed'
2526
Aaron Gable9ab38c62017-04-06 14:36:33 -07002527 if data['labels'].get('Commit-Queue', {}).get('approved'):
2528 # The section will have an "approved" subsection if anyone has voted
2529 # the maximum value on the label.
2530 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002531
Aaron Gable9ab38c62017-04-06 14:36:33 -07002532 if data['labels'].get('Code-Review', {}).get('approved'):
2533 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002534
2535 if not data.get('reviewers', {}).get('REVIEWER', []):
2536 return 'unsent'
2537
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002538 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002539 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2540 last_message_author = messages.pop().get('author', {})
2541 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002542 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2543 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002544 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002545 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002546 if last_message_author.get('_account_id') == owner:
2547 # Most recent message was by owner.
2548 return 'waiting'
2549 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002550 # Some reply from non-owner.
2551 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002552
2553 # Somehow there are no messages even though there are reviewers.
2554 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002555
2556 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002557 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002558 patchset = data['revisions'][data['current_revision']]['_number']
2559 self.SetPatchset(patchset)
2560 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002561
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002562 def FetchDescription(self, force=False):
2563 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2564 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002565 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002566 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002567
dsansomee2d6fd92016-09-08 00:10:47 -07002568 def UpdateDescriptionRemote(self, description, force=False):
2569 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2570 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002571 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002572 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002573 'unpublished edit. Either publish the edit in the Gerrit web UI '
2574 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002575
2576 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2577 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002578 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002579 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002580
Aaron Gable636b13f2017-07-14 10:42:48 -07002581 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002582 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002583 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002584
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002585 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002586 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002587 messages = self._GetChangeDetail(
2588 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2589 file_comments = gerrit_util.GetChangeComments(
2590 self._GetGerritHost(), self.GetIssue())
2591
2592 # Build dictionary of file comments for easy access and sorting later.
2593 # {author+date: {path: {patchset: {line: url+message}}}}
2594 comments = collections.defaultdict(
2595 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2596 for path, line_comments in file_comments.iteritems():
2597 for comment in line_comments:
2598 if comment.get('tag', '').startswith('autogenerated'):
2599 continue
2600 key = (comment['author']['email'], comment['updated'])
2601 if comment.get('side', 'REVISION') == 'PARENT':
2602 patchset = 'Base'
2603 else:
2604 patchset = 'PS%d' % comment['patch_set']
2605 line = comment.get('line', 0)
2606 url = ('https://%s/c/%s/%s/%s#%s%s' %
2607 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2608 'b' if comment.get('side') == 'PARENT' else '',
2609 str(line) if line else ''))
2610 comments[key][path][patchset][line] = (url, comment['message'])
2611
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002612 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002613 for msg in messages:
2614 # Don't bother showing autogenerated messages.
2615 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2616 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002617 # Gerrit spits out nanoseconds.
2618 assert len(msg['date'].split('.')[-1]) == 9
2619 date = datetime.datetime.strptime(msg['date'][:-3],
2620 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002621 message = msg['message']
2622 key = (msg['author']['email'], msg['date'])
2623 if key in comments:
2624 message += '\n'
2625 for path, patchsets in sorted(comments.get(key, {}).items()):
2626 if readable:
2627 message += '\n%s' % path
2628 for patchset, lines in sorted(patchsets.items()):
2629 for line, (url, content) in sorted(lines.items()):
2630 if line:
2631 line_str = 'Line %d' % line
2632 path_str = '%s:%d:' % (path, line)
2633 else:
2634 line_str = 'File comment'
2635 path_str = '%s:0:' % path
2636 if readable:
2637 message += '\n %s, %s: %s' % (patchset, line_str, url)
2638 message += '\n %s\n' % content
2639 else:
2640 message += '\n%s ' % path_str
2641 message += '\n%s\n' % content
2642
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002643 summary.append(_CommentSummary(
2644 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002645 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002646 sender=msg['author']['email'],
2647 # These could be inferred from the text messages and correlated with
2648 # Code-Review label maximum, however this is not reliable.
2649 # Leaving as is until the need arises.
2650 approval=False,
2651 disapproval=False,
2652 ))
2653 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002654
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002655 def CloseIssue(self):
2656 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2657
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002658 def SubmitIssue(self, wait_for_merge=True):
2659 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2660 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002661
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002662 def _GetChangeDetail(self, options=None, issue=None,
2663 no_cache=False):
2664 """Returns details of the issue by querying Gerrit and caching results.
2665
2666 If fresh data is needed, set no_cache=True which will clear cache and
2667 thus new data will be fetched from Gerrit.
2668 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002669 options = options or []
2670 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002671 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002672
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002673 # Optimization to avoid multiple RPCs:
2674 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2675 'CURRENT_COMMIT' not in options):
2676 options.append('CURRENT_COMMIT')
2677
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002678 # Normalize issue and options for consistent keys in cache.
2679 issue = str(issue)
2680 options = [o.upper() for o in options]
2681
2682 # Check in cache first unless no_cache is True.
2683 if no_cache:
2684 self._detail_cache.pop(issue, None)
2685 else:
2686 options_set = frozenset(options)
2687 for cached_options_set, data in self._detail_cache.get(issue, []):
2688 # Assumption: data fetched before with extra options is suitable
2689 # for return for a smaller set of options.
2690 # For example, if we cached data for
2691 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2692 # and request is for options=[CURRENT_REVISION],
2693 # THEN we can return prior cached data.
2694 if options_set.issubset(cached_options_set):
2695 return data
2696
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002697 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002698 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002699 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002700 except gerrit_util.GerritError as e:
2701 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002702 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002703 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002704
2705 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002706 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002707
agable32978d92016-11-01 12:55:02 -07002708 def _GetChangeCommit(self, issue=None):
2709 issue = issue or self.GetIssue()
2710 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002711 try:
2712 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2713 except gerrit_util.GerritError as e:
2714 if e.http_status == 404:
2715 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2716 raise
agable32978d92016-11-01 12:55:02 -07002717 return data
2718
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002719 def CMDLand(self, force, bypass_hooks, verbose):
2720 if git_common.is_dirty_git_tree('land'):
2721 return 1
tandriid60367b2016-06-22 05:25:12 -07002722 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2723 if u'Commit-Queue' in detail.get('labels', {}):
2724 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002725 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2726 'which can test and land changes for you. '
2727 'Are you sure you wish to bypass it?\n',
2728 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002729
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002730 differs = True
tandriic4344b52016-08-29 06:04:54 -07002731 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002732 # Note: git diff outputs nothing if there is no diff.
2733 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002734 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002735 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002736 if detail['current_revision'] == last_upload:
2737 differs = False
2738 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002739 print('WARNING: Local branch contents differ from latest uploaded '
2740 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002741 if differs:
2742 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002743 confirm_or_exit(
2744 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2745 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002746 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002747 elif not bypass_hooks:
2748 hook_results = self.RunHook(
2749 committing=True,
2750 may_prompt=not force,
2751 verbose=verbose,
2752 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2753 if not hook_results.should_continue():
2754 return 1
2755
2756 self.SubmitIssue(wait_for_merge=True)
2757 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002758 links = self._GetChangeCommit().get('web_links', [])
2759 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002760 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002761 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002762 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002763 return 0
2764
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002765 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002766 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002767 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002768 assert not directory
2769 assert parsed_issue_arg.valid
2770
2771 self._changelist.issue = parsed_issue_arg.issue
2772
2773 if parsed_issue_arg.hostname:
2774 self._gerrit_host = parsed_issue_arg.hostname
2775 self._gerrit_server = 'https://%s' % self._gerrit_host
2776
tandriic2405f52016-10-10 08:13:15 -07002777 try:
2778 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002779 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002780 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002781
2782 if not parsed_issue_arg.patchset:
2783 # Use current revision by default.
2784 revision_info = detail['revisions'][detail['current_revision']]
2785 patchset = int(revision_info['_number'])
2786 else:
2787 patchset = parsed_issue_arg.patchset
2788 for revision_info in detail['revisions'].itervalues():
2789 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2790 break
2791 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002792 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002793 (parsed_issue_arg.patchset, self.GetIssue()))
2794
Aaron Gable697a91b2018-01-19 15:20:15 -08002795 remote_url = self._changelist.GetRemoteUrl()
2796 if remote_url.endswith('.git'):
2797 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002798 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002799
2800 if remote_url != fetch_info['url']:
2801 DieWithError('Trying to patch a change from %s but this repo appears '
2802 'to be %s.' % (fetch_info['url'], remote_url))
2803
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002804 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002805
Aaron Gable62619a32017-06-16 08:22:09 -07002806 if force:
2807 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2808 print('Checked out commit for change %i patchset %i locally' %
2809 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002810 elif nocommit:
2811 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2812 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002813 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002814 RunGit(['cherry-pick', 'FETCH_HEAD'])
2815 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002816 (parsed_issue_arg.issue, patchset))
2817 print('Note: this created a local commit which does not have '
2818 'the same hash as the one uploaded for review. This will make '
2819 'uploading changes based on top of this branch difficult.\n'
2820 'If you want to do that, use "git cl patch --force" instead.')
2821
Stefan Zagerd08043c2017-10-12 12:07:02 -07002822 if self.GetBranch():
2823 self.SetIssue(parsed_issue_arg.issue)
2824 self.SetPatchset(patchset)
2825 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2826 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2827 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2828 else:
2829 print('WARNING: You are in detached HEAD state.\n'
2830 'The patch has been applied to your checkout, but you will not be '
2831 'able to upload a new patch set to the gerrit issue.\n'
2832 'Try using the \'-b\' option if you would like to work on a '
2833 'branch and/or upload a new patch set.')
2834
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002835 return 0
2836
2837 @staticmethod
2838 def ParseIssueURL(parsed_url):
2839 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2840 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002841 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2842 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002843 # Short urls like https://domain/<issue_number> can be used, but don't allow
2844 # specifying the patchset (you'd 404), but we allow that here.
2845 if parsed_url.path == '/':
2846 part = parsed_url.fragment
2847 else:
2848 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002849 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002850 if match:
2851 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002852 issue=int(match.group(3)),
2853 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002854 hostname=parsed_url.netloc,
2855 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002856 return None
2857
tandrii16e0b4e2016-06-07 10:34:28 -07002858 def _GerritCommitMsgHookCheck(self, offer_removal):
2859 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2860 if not os.path.exists(hook):
2861 return
2862 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2863 # custom developer made one.
2864 data = gclient_utils.FileRead(hook)
2865 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2866 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002867 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002868 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002869 'and may interfere with it in subtle ways.\n'
2870 'We recommend you remove the commit-msg hook.')
2871 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002872 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002873 gclient_utils.rm_file_or_tree(hook)
2874 print('Gerrit commit-msg hook removed.')
2875 else:
2876 print('OK, will keep Gerrit commit-msg hook in place.')
2877
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002878 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002879 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002880 if options.squash and options.no_squash:
2881 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002882
2883 if not options.squash and not options.no_squash:
2884 # Load default for user, repo, squash=true, in this order.
2885 options.squash = settings.GetSquashGerritUploads()
2886 elif options.no_squash:
2887 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002888
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002889 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002890 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002891
Aaron Gableb56ad332017-01-06 15:24:31 -08002892 # This may be None; default fallback value is determined in logic below.
2893 title = options.title
2894
Dominic Battre7d1c4842017-10-27 09:17:28 +02002895 # Extract bug number from branch name.
2896 bug = options.bug
2897 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2898 if not bug and match:
2899 bug = match.group(1)
2900
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002901 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002902 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002903 if self.GetIssue():
2904 # Try to get the message from a previous upload.
2905 message = self.GetDescription()
2906 if not message:
2907 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002908 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002909 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002910 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002911 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002912 # When uploading a subsequent patchset, -m|--message is taken
2913 # as the patchset title if --title was not provided.
2914 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002915 else:
2916 default_title = RunGit(
2917 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002918 if options.force:
2919 title = default_title
2920 else:
2921 title = ask_for_data(
2922 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923 change_id = self._GetChangeDetail()['change_id']
2924 while True:
2925 footer_change_ids = git_footers.get_footer_change_id(message)
2926 if footer_change_ids == [change_id]:
2927 break
2928 if not footer_change_ids:
2929 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002930 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002931 continue
2932 # There is already a valid footer but with different or several ids.
2933 # Doing this automatically is non-trivial as we don't want to lose
2934 # existing other footers, yet we want to append just 1 desired
2935 # Change-Id. Thus, just create a new footer, but let user verify the
2936 # new description.
2937 message = '%s\n\nChange-Id: %s' % (message, change_id)
2938 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002939 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002940 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002941 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002942 'Please, check the proposed correction to the description, '
2943 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2944 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2945 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002946 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002947 if not options.force:
2948 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002949 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002950 message = change_desc.description
2951 if not message:
2952 DieWithError("Description is empty. Aborting...")
2953 # Continue the while loop.
2954 # Sanity check of this code - we should end up with proper message
2955 # footer.
2956 assert [change_id] == git_footers.get_footer_change_id(message)
2957 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002958 else: # if not self.GetIssue()
2959 if options.message:
2960 message = options.message
2961 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002962 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002963 if options.title:
2964 message = options.title + '\n\n' + message
2965 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002966
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002967 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002968 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002969 # On first upload, patchset title is always this string, while
2970 # --title flag gets converted to first line of message.
2971 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002972 if not change_desc.description:
2973 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002974 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002975 if len(change_ids) > 1:
2976 DieWithError('too many Change-Id footers, at most 1 allowed.')
2977 if not change_ids:
2978 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002979 change_desc.set_description(git_footers.add_footer_change_id(
2980 change_desc.description,
2981 GenerateGerritChangeId(change_desc.description)))
2982 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002983 assert len(change_ids) == 1
2984 change_id = change_ids[0]
2985
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002986 if options.reviewers or options.tbrs or options.add_owners_to:
2987 change_desc.update_reviewers(options.reviewers, options.tbrs,
2988 options.add_owners_to, change)
2989
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002990 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002991 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2992 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002993 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002994 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2995 desc_tempfile.write(change_desc.description)
2996 desc_tempfile.close()
2997 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2998 '-F', desc_tempfile.name]).strip()
2999 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003000 else:
3001 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003002 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003003 if not change_desc.description:
3004 DieWithError("Description is empty. Aborting...")
3005
3006 if not git_footers.get_footer_change_id(change_desc.description):
3007 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003008 change_desc.set_description(
3009 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003010 if options.reviewers or options.tbrs or options.add_owners_to:
3011 change_desc.update_reviewers(options.reviewers, options.tbrs,
3012 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003013 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003014 # For no-squash mode, we assume the remote called "origin" is the one we
3015 # want. It is not worthwhile to support different workflows for
3016 # no-squash mode.
3017 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003018 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3019
3020 assert change_desc
3021 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3022 ref_to_push)]).splitlines()
3023 if len(commits) > 1:
3024 print('WARNING: This will upload %d commits. Run the following command '
3025 'to see which commits will be uploaded: ' % len(commits))
3026 print('git log %s..%s' % (parent, ref_to_push))
3027 print('You can also use `git squash-branch` to squash these into a '
3028 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003029 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003030
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003031 if options.reviewers or options.tbrs or options.add_owners_to:
3032 change_desc.update_reviewers(options.reviewers, options.tbrs,
3033 options.add_owners_to, change)
3034
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003035 # Extra options that can be specified at push time. Doc:
3036 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003037 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003038
Aaron Gable844cf292017-06-28 11:32:59 -07003039 # By default, new changes are started in WIP mode, and subsequent patchsets
3040 # don't send email. At any time, passing --send-mail will mark the change
3041 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003042 if options.send_mail:
3043 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003044 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003045 elif not self.GetIssue():
3046 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003047 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003048 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003049
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003050 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003051 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003052
Aaron Gable9b713dd2016-12-14 16:04:21 -08003053 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003054 # Punctuation and whitespace in |title| must be percent-encoded.
3055 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003056
agablec6787972016-09-09 16:13:34 -07003057 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003058 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003059
rmistry9eadede2016-09-19 11:22:43 -07003060 if options.topic:
3061 # Documentation on Gerrit topics is here:
3062 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003063 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003064
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003065 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003066 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003067 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003068 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003069 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3070
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003071 refspec_suffix = ''
3072 if refspec_opts:
3073 refspec_suffix = '%' + ','.join(refspec_opts)
3074 assert ' ' not in refspec_suffix, (
3075 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3076 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3077
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003078 try:
3079 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003080 ['git', 'push', self.GetRemoteUrl(), refspec],
3081 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003082 # Flush after every line: useful for seeing progress when running as
3083 # recipe.
3084 filter_fn=lambda _: sys.stdout.flush())
3085 except subprocess2.CalledProcessError:
3086 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003087 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003088 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003089 'credential problems:\n'
3090 ' git cl creds-check\n',
3091 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003092
3093 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003094 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003095 change_numbers = [m.group(1)
3096 for m in map(regex.match, push_stdout.splitlines())
3097 if m]
3098 if len(change_numbers) != 1:
3099 DieWithError(
3100 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003101 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003102 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003103 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003104
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003105 reviewers = sorted(change_desc.get_reviewers())
3106
tandrii88189772016-09-29 04:29:57 -07003107 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003108 if not options.private:
3109 cc = self.GetCCList().split(',')
3110 else:
3111 cc = []
tandrii88189772016-09-29 04:29:57 -07003112 if options.cc:
3113 cc.extend(options.cc)
3114 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003115 if change_desc.get_cced():
3116 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003117
3118 gerrit_util.AddReviewers(
3119 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3120 notify=bool(options.send_mail))
3121
Aaron Gablefd238082017-06-07 13:42:34 -07003122 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003123 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3124 score = 1
3125 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3126 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3127 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003128 gerrit_util.SetReview(
3129 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003130 msg='Self-approving for TBR',
3131 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003132
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003133 return 0
3134
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003135 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3136 change_desc):
3137 """Computes parent of the generated commit to be uploaded to Gerrit.
3138
3139 Returns revision or a ref name.
3140 """
3141 if custom_cl_base:
3142 # Try to avoid creating additional unintended CLs when uploading, unless
3143 # user wants to take this risk.
3144 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3145 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3146 local_ref_of_target_remote])
3147 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003148 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003149 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3150 'If you proceed with upload, more than 1 CL may be created by '
3151 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3152 'If you are certain that specified base `%s` has already been '
3153 'uploaded to Gerrit as another CL, you may proceed.\n' %
3154 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3155 if not force:
3156 confirm_or_exit(
3157 'Do you take responsibility for cleaning up potential mess '
3158 'resulting from proceeding with upload?',
3159 action='upload')
3160 return custom_cl_base
3161
Aaron Gablef97e33d2017-03-30 15:44:27 -07003162 if remote != '.':
3163 return self.GetCommonAncestorWithUpstream()
3164
3165 # If our upstream branch is local, we base our squashed commit on its
3166 # squashed version.
3167 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3168
Aaron Gablef97e33d2017-03-30 15:44:27 -07003169 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003170 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003171
3172 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003173 # TODO(tandrii): consider checking parent change in Gerrit and using its
3174 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3175 # the tree hash of the parent branch. The upside is less likely bogus
3176 # requests to reupload parent change just because it's uploadhash is
3177 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003178 parent = RunGit(['config',
3179 'branch.%s.gerritsquashhash' % upstream_branch_name],
3180 error_ok=True).strip()
3181 # Verify that the upstream branch has been uploaded too, otherwise
3182 # Gerrit will create additional CLs when uploading.
3183 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3184 RunGitSilent(['rev-parse', parent + ':'])):
3185 DieWithError(
3186 '\nUpload upstream branch %s first.\n'
3187 'It is likely that this branch has been rebased since its last '
3188 'upload, so you just need to upload it again.\n'
3189 '(If you uploaded it with --no-squash, then branch dependencies '
3190 'are not supported, and you should reupload with --squash.)'
3191 % upstream_branch_name,
3192 change_desc)
3193 return parent
3194
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003195 def _AddChangeIdToCommitMessage(self, options, args):
3196 """Re-commits using the current message, assumes the commit hook is in
3197 place.
3198 """
3199 log_desc = options.message or CreateDescriptionFromLog(args)
3200 git_command = ['commit', '--amend', '-m', log_desc]
3201 RunGit(git_command)
3202 new_log_desc = CreateDescriptionFromLog(args)
3203 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003204 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003205 return new_log_desc
3206 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003207 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003208
Ravi Mistry31e7d562018-04-02 12:53:57 -04003209 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3210 """Sets labels on the change based on the provided flags."""
3211 labels = {}
3212 notify = None;
3213 if enable_auto_submit:
3214 labels['Auto-Submit'] = 1
3215 if use_commit_queue:
3216 labels['Commit-Queue'] = 2
3217 elif cq_dry_run:
3218 labels['Commit-Queue'] = 1
3219 notify = False
3220 if labels:
3221 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3222 labels=labels, notify=notify)
3223
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003224 def SetCQState(self, new_state):
3225 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003226 vote_map = {
3227 _CQState.NONE: 0,
3228 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003229 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003230 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003231 labels = {'Commit-Queue': vote_map[new_state]}
3232 notify = False if new_state == _CQState.DRY_RUN else None
3233 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3234 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003235
tandriie113dfd2016-10-11 10:20:12 -07003236 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003237 try:
3238 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003239 except GerritChangeNotExists:
3240 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003241
3242 if data['status'] in ('ABANDONED', 'MERGED'):
3243 return 'CL %s is closed' % self.GetIssue()
3244
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003245 def GetTryJobProperties(self, patchset=None):
3246 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003247 data = self._GetChangeDetail(['ALL_REVISIONS'])
3248 patchset = int(patchset or self.GetPatchset())
3249 assert patchset
3250 revision_data = None # Pylint wants it to be defined.
3251 for revision_data in data['revisions'].itervalues():
3252 if int(revision_data['_number']) == patchset:
3253 break
3254 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003255 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003256 (patchset, self.GetIssue()))
3257 return {
3258 'patch_issue': self.GetIssue(),
3259 'patch_set': patchset or self.GetPatchset(),
3260 'patch_project': data['project'],
3261 'patch_storage': 'gerrit',
3262 'patch_ref': revision_data['fetch']['http']['ref'],
3263 'patch_repository_url': revision_data['fetch']['http']['url'],
3264 'patch_gerrit_url': self.GetCodereviewServer(),
3265 }
tandriie113dfd2016-10-11 10:20:12 -07003266
tandriide281ae2016-10-12 06:02:30 -07003267 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003268 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003269
Edward Lemur707d70b2018-02-07 00:50:14 +01003270 def GetReviewers(self):
3271 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3272 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3273
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003274
3275_CODEREVIEW_IMPLEMENTATIONS = {
3276 'rietveld': _RietveldChangelistImpl,
3277 'gerrit': _GerritChangelistImpl,
3278}
3279
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003280
iannuccie53c9352016-08-17 14:40:40 -07003281def _add_codereview_issue_select_options(parser, extra=""):
3282 _add_codereview_select_options(parser)
3283
3284 text = ('Operate on this issue number instead of the current branch\'s '
3285 'implicit issue.')
3286 if extra:
3287 text += ' '+extra
3288 parser.add_option('-i', '--issue', type=int, help=text)
3289
3290
3291def _process_codereview_issue_select_options(parser, options):
3292 _process_codereview_select_options(parser, options)
3293 if options.issue is not None and not options.forced_codereview:
3294 parser.error('--issue must be specified with either --rietveld or --gerrit')
3295
3296
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003297def _add_codereview_select_options(parser):
3298 """Appends --gerrit and --rietveld options to force specific codereview."""
3299 parser.codereview_group = optparse.OptionGroup(
3300 parser, 'EXPERIMENTAL! Codereview override options')
3301 parser.add_option_group(parser.codereview_group)
3302 parser.codereview_group.add_option(
3303 '--gerrit', action='store_true',
3304 help='Force the use of Gerrit for codereview')
3305 parser.codereview_group.add_option(
3306 '--rietveld', action='store_true',
3307 help='Force the use of Rietveld for codereview')
3308
3309
3310def _process_codereview_select_options(parser, options):
3311 if options.gerrit and options.rietveld:
3312 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3313 options.forced_codereview = None
3314 if options.gerrit:
3315 options.forced_codereview = 'gerrit'
3316 elif options.rietveld:
3317 options.forced_codereview = 'rietveld'
3318
3319
tandriif9aefb72016-07-01 09:06:51 -07003320def _get_bug_line_values(default_project, bugs):
3321 """Given default_project and comma separated list of bugs, yields bug line
3322 values.
3323
3324 Each bug can be either:
3325 * a number, which is combined with default_project
3326 * string, which is left as is.
3327
3328 This function may produce more than one line, because bugdroid expects one
3329 project per line.
3330
3331 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3332 ['v8:123', 'chromium:789']
3333 """
3334 default_bugs = []
3335 others = []
3336 for bug in bugs.split(','):
3337 bug = bug.strip()
3338 if bug:
3339 try:
3340 default_bugs.append(int(bug))
3341 except ValueError:
3342 others.append(bug)
3343
3344 if default_bugs:
3345 default_bugs = ','.join(map(str, default_bugs))
3346 if default_project:
3347 yield '%s:%s' % (default_project, default_bugs)
3348 else:
3349 yield default_bugs
3350 for other in sorted(others):
3351 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3352 yield other
3353
3354
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003355class ChangeDescription(object):
3356 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003357 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003358 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003359 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003360 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003361 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3362 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3363 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3364 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003365
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003366 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003367 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003368
agable@chromium.org42c20792013-09-12 17:34:49 +00003369 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003370 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003371 return '\n'.join(self._description_lines)
3372
3373 def set_description(self, desc):
3374 if isinstance(desc, basestring):
3375 lines = desc.splitlines()
3376 else:
3377 lines = [line.rstrip() for line in desc]
3378 while lines and not lines[0]:
3379 lines.pop(0)
3380 while lines and not lines[-1]:
3381 lines.pop(-1)
3382 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003383
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003384 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3385 """Rewrites the R=/TBR= line(s) as a single line each.
3386
3387 Args:
3388 reviewers (list(str)) - list of additional emails to use for reviewers.
3389 tbrs (list(str)) - list of additional emails to use for TBRs.
3390 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3391 the change that are missing OWNER coverage. If this is not None, you
3392 must also pass a value for `change`.
3393 change (Change) - The Change that should be used for OWNERS lookups.
3394 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003395 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003396 assert isinstance(tbrs, list), tbrs
3397
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003398 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003399 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003400
3401 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003402 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003403
3404 reviewers = set(reviewers)
3405 tbrs = set(tbrs)
3406 LOOKUP = {
3407 'TBR': tbrs,
3408 'R': reviewers,
3409 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003410
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003411 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003412 regexp = re.compile(self.R_LINE)
3413 matches = [regexp.match(line) for line in self._description_lines]
3414 new_desc = [l for i, l in enumerate(self._description_lines)
3415 if not matches[i]]
3416 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003417
agable@chromium.org42c20792013-09-12 17:34:49 +00003418 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003419
3420 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003421 for match in matches:
3422 if not match:
3423 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003424 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3425
3426 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003427 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003428 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003429 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003430 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003431 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003432 LOOKUP[add_owners_to].update(
3433 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003434
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003435 # If any folks ended up in both groups, remove them from tbrs.
3436 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003437
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003438 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3439 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003440
3441 # Put the new lines in the description where the old first R= line was.
3442 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3443 if 0 <= line_loc < len(self._description_lines):
3444 if new_tbr_line:
3445 self._description_lines.insert(line_loc, new_tbr_line)
3446 if new_r_line:
3447 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003448 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003449 if new_r_line:
3450 self.append_footer(new_r_line)
3451 if new_tbr_line:
3452 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003453
Aaron Gable3a16ed12017-03-23 10:51:55 -07003454 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003455 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003456 self.set_description([
3457 '# Enter a description of the change.',
3458 '# This will be displayed on the codereview site.',
3459 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003460 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003461 '--------------------',
3462 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003463
agable@chromium.org42c20792013-09-12 17:34:49 +00003464 regexp = re.compile(self.BUG_LINE)
3465 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003466 prefix = settings.GetBugPrefix()
3467 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003468 if git_footer:
3469 self.append_footer('Bug: %s' % ', '.join(values))
3470 else:
3471 for value in values:
3472 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003473
agable@chromium.org42c20792013-09-12 17:34:49 +00003474 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003475 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003476 if not content:
3477 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003478 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003479
Bruce Dawson2377b012018-01-11 16:46:49 -08003480 # Strip off comments and default inserted "Bug:" line.
3481 clean_lines = [line.rstrip() for line in lines if not
3482 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003483 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003484 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003485 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003486
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003487 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003488 """Adds a footer line to the description.
3489
3490 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3491 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3492 that Gerrit footers are always at the end.
3493 """
3494 parsed_footer_line = git_footers.parse_footer(line)
3495 if parsed_footer_line:
3496 # Line is a gerrit footer in the form: Footer-Key: any value.
3497 # Thus, must be appended observing Gerrit footer rules.
3498 self.set_description(
3499 git_footers.add_footer(self.description,
3500 key=parsed_footer_line[0],
3501 value=parsed_footer_line[1]))
3502 return
3503
3504 if not self._description_lines:
3505 self._description_lines.append(line)
3506 return
3507
3508 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3509 if gerrit_footers:
3510 # git_footers.split_footers ensures that there is an empty line before
3511 # actual (gerrit) footers, if any. We have to keep it that way.
3512 assert top_lines and top_lines[-1] == ''
3513 top_lines, separator = top_lines[:-1], top_lines[-1:]
3514 else:
3515 separator = [] # No need for separator if there are no gerrit_footers.
3516
3517 prev_line = top_lines[-1] if top_lines else ''
3518 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3519 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3520 top_lines.append('')
3521 top_lines.append(line)
3522 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003523
tandrii99a72f22016-08-17 14:33:24 -07003524 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003525 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003526 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003527 reviewers = [match.group(2).strip()
3528 for match in matches
3529 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003530 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003531
bradnelsond975b302016-10-23 12:20:23 -07003532 def get_cced(self):
3533 """Retrieves the list of reviewers."""
3534 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3535 cced = [match.group(2).strip() for match in matches if match]
3536 return cleanup_list(cced)
3537
Nodir Turakulov23b82142017-11-16 11:04:25 -08003538 def get_hash_tags(self):
3539 """Extracts and sanitizes a list of Gerrit hashtags."""
3540 subject = (self._description_lines or ('',))[0]
3541 subject = re.sub(
3542 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3543
3544 tags = []
3545 start = 0
3546 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3547 while True:
3548 m = bracket_exp.match(subject, start)
3549 if not m:
3550 break
3551 tags.append(self.sanitize_hash_tag(m.group(1)))
3552 start = m.end()
3553
3554 if not tags:
3555 # Try "Tag: " prefix.
3556 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3557 if m:
3558 tags.append(self.sanitize_hash_tag(m.group(1)))
3559 return tags
3560
3561 @classmethod
3562 def sanitize_hash_tag(cls, tag):
3563 """Returns a sanitized Gerrit hash tag.
3564
3565 A sanitized hashtag can be used as a git push refspec parameter value.
3566 """
3567 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3568
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003569 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3570 """Updates this commit description given the parent.
3571
3572 This is essentially what Gnumbd used to do.
3573 Consult https://goo.gl/WMmpDe for more details.
3574 """
3575 assert parent_msg # No, orphan branch creation isn't supported.
3576 assert parent_hash
3577 assert dest_ref
3578 parent_footer_map = git_footers.parse_footers(parent_msg)
3579 # This will also happily parse svn-position, which GnumbD is no longer
3580 # supporting. While we'd generate correct footers, the verifier plugin
3581 # installed in Gerrit will block such commit (ie git push below will fail).
3582 parent_position = git_footers.get_position(parent_footer_map)
3583
3584 # Cherry-picks may have last line obscuring their prior footers,
3585 # from git_footers perspective. This is also what Gnumbd did.
3586 cp_line = None
3587 if (self._description_lines and
3588 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3589 cp_line = self._description_lines.pop()
3590
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003591 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003592
3593 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3594 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003595 for i, line in enumerate(footer_lines):
3596 k, v = git_footers.parse_footer(line) or (None, None)
3597 if k and k.startswith('Cr-'):
3598 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003599
3600 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003601 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003602 if parent_position[0] == dest_ref:
3603 # Same branch as parent.
3604 number = int(parent_position[1]) + 1
3605 else:
3606 number = 1 # New branch, and extra lineage.
3607 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3608 int(parent_position[1])))
3609
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003610 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3611 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003612
3613 self._description_lines = top_lines
3614 if cp_line:
3615 self._description_lines.append(cp_line)
3616 if self._description_lines[-1] != '':
3617 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003618 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003619
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003620
Aaron Gablea1bab272017-04-11 16:38:18 -07003621def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003622 """Retrieves the reviewers that approved a CL from the issue properties with
3623 messages.
3624
3625 Note that the list may contain reviewers that are not committer, thus are not
3626 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003627
3628 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003629 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003630 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003631 return sorted(
3632 set(
3633 message['sender']
3634 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003635 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003636 )
3637 )
3638
3639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640def FindCodereviewSettingsFile(filename='codereview.settings'):
3641 """Finds the given file starting in the cwd and going up.
3642
3643 Only looks up to the top of the repository unless an
3644 'inherit-review-settings-ok' file exists in the root of the repository.
3645 """
3646 inherit_ok_file = 'inherit-review-settings-ok'
3647 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003648 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3650 root = '/'
3651 while True:
3652 if filename in os.listdir(cwd):
3653 if os.path.isfile(os.path.join(cwd, filename)):
3654 return open(os.path.join(cwd, filename))
3655 if cwd == root:
3656 break
3657 cwd = os.path.dirname(cwd)
3658
3659
3660def LoadCodereviewSettingsFromFile(fileobj):
3661 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003662 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664 def SetProperty(name, setting, unset_error_ok=False):
3665 fullname = 'rietveld.' + name
3666 if setting in keyvals:
3667 RunGit(['config', fullname, keyvals[setting]])
3668 else:
3669 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3670
tandrii48df5812016-10-17 03:55:37 -07003671 if not keyvals.get('GERRIT_HOST', False):
3672 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003673 # Only server setting is required. Other settings can be absent.
3674 # In that case, we ignore errors raised during option deletion attempt.
3675 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003676 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3678 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003679 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003680 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3681 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003682 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003683 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3684 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003685
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003686 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003687 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003688
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003689 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003690 RunGit(['config', 'gerrit.squash-uploads',
3691 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003692
tandrii@chromium.org28253532016-04-14 13:46:56 +00003693 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003694 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003695 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003698 # should be of the form
3699 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3700 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3702 keyvals['ORIGIN_URL_CONFIG']])
3703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003704
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003705def urlretrieve(source, destination):
3706 """urllib is broken for SSL connections via a proxy therefore we
3707 can't use urllib.urlretrieve()."""
3708 with open(destination, 'w') as f:
3709 f.write(urllib2.urlopen(source).read())
3710
3711
ukai@chromium.org712d6102013-11-27 00:52:58 +00003712def hasSheBang(fname):
3713 """Checks fname is a #! script."""
3714 with open(fname) as f:
3715 return f.read(2).startswith('#!')
3716
3717
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003718# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3719def DownloadHooks(*args, **kwargs):
3720 pass
3721
3722
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003723def DownloadGerritHook(force):
3724 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003725
3726 Args:
3727 force: True to update hooks. False to install hooks if not present.
3728 """
3729 if not settings.GetIsGerrit():
3730 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003731 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003732 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3733 if not os.access(dst, os.X_OK):
3734 if os.path.exists(dst):
3735 if not force:
3736 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003737 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003738 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003739 if not hasSheBang(dst):
3740 DieWithError('Not a script: %s\n'
3741 'You need to download from\n%s\n'
3742 'into .git/hooks/commit-msg and '
3743 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003744 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3745 except Exception:
3746 if os.path.exists(dst):
3747 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003748 DieWithError('\nFailed to download hooks.\n'
3749 'You need to download from\n%s\n'
3750 'into .git/hooks/commit-msg and '
3751 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003752
3753
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003754def GetRietveldCodereviewSettingsInteractively():
3755 """Prompt the user for settings."""
3756 server = settings.GetDefaultServerUrl(error_ok=True)
3757 prompt = 'Rietveld server (host[:port])'
3758 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3759 newserver = ask_for_data(prompt + ':')
3760 if not server and not newserver:
3761 newserver = DEFAULT_SERVER
3762 if newserver:
3763 newserver = gclient_utils.UpgradeToHttps(newserver)
3764 if newserver != server:
3765 RunGit(['config', 'rietveld.server', newserver])
3766
3767 def SetProperty(initial, caption, name, is_url):
3768 prompt = caption
3769 if initial:
3770 prompt += ' ("x" to clear) [%s]' % initial
3771 new_val = ask_for_data(prompt + ':')
3772 if new_val == 'x':
3773 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3774 elif new_val:
3775 if is_url:
3776 new_val = gclient_utils.UpgradeToHttps(new_val)
3777 if new_val != initial:
3778 RunGit(['config', 'rietveld.' + name, new_val])
3779
3780 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3781 SetProperty(settings.GetDefaultPrivateFlag(),
3782 'Private flag (rietveld only)', 'private', False)
3783 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3784 'tree-status-url', False)
3785 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3786 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3787 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3788 'run-post-upload-hook', False)
3789
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003790
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003791class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003792 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003793
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003794 _GOOGLESOURCE = 'googlesource.com'
3795
3796 def __init__(self):
3797 # Cached list of [host, identity, source], where source is either
3798 # .gitcookies or .netrc.
3799 self._all_hosts = None
3800
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003801 def ensure_configured_gitcookies(self):
3802 """Runs checks and suggests fixes to make git use .gitcookies from default
3803 path."""
3804 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3805 configured_path = RunGitSilent(
3806 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003807 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003808 if configured_path:
3809 self._ensure_default_gitcookies_path(configured_path, default)
3810 else:
3811 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003812
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003813 @staticmethod
3814 def _ensure_default_gitcookies_path(configured_path, default_path):
3815 assert configured_path
3816 if configured_path == default_path:
3817 print('git is already configured to use your .gitcookies from %s' %
3818 configured_path)
3819 return
3820
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003821 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003822 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3823 (configured_path, default_path))
3824
3825 if not os.path.exists(configured_path):
3826 print('However, your configured .gitcookies file is missing.')
3827 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3828 action='reconfigure')
3829 RunGit(['config', '--global', 'http.cookiefile', default_path])
3830 return
3831
3832 if os.path.exists(default_path):
3833 print('WARNING: default .gitcookies file already exists %s' %
3834 default_path)
3835 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3836 default_path)
3837
3838 confirm_or_exit('Move existing .gitcookies to default location?',
3839 action='move')
3840 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003841 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003842 print('Moved and reconfigured git to use .gitcookies from %s' %
3843 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003844
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003845 @staticmethod
3846 def _configure_gitcookies_path(default_path):
3847 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3848 if os.path.exists(netrc_path):
3849 print('You seem to be using outdated .netrc for git credentials: %s' %
3850 netrc_path)
3851 print('This tool will guide you through setting up recommended '
3852 '.gitcookies store for git credentials.\n'
3853 '\n'
3854 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3855 ' git config --global --unset http.cookiefile\n'
3856 ' mv %s %s.backup\n\n' % (default_path, default_path))
3857 confirm_or_exit(action='setup .gitcookies')
3858 RunGit(['config', '--global', 'http.cookiefile', default_path])
3859 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003860
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003861 def get_hosts_with_creds(self, include_netrc=False):
3862 if self._all_hosts is None:
3863 a = gerrit_util.CookiesAuthenticator()
3864 self._all_hosts = [
3865 (h, u, s)
3866 for h, u, s in itertools.chain(
3867 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3868 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3869 )
3870 if h.endswith(self._GOOGLESOURCE)
3871 ]
3872
3873 if include_netrc:
3874 return self._all_hosts
3875 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3876
3877 def print_current_creds(self, include_netrc=False):
3878 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3879 if not hosts:
3880 print('No Git/Gerrit credentials found')
3881 return
3882 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3883 header = [('Host', 'User', 'Which file'),
3884 ['=' * l for l in lengths]]
3885 for row in (header + hosts):
3886 print('\t'.join((('%%+%ds' % l) % s)
3887 for l, s in zip(lengths, row)))
3888
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003889 @staticmethod
3890 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003891 """Parses identity "git-<username>.domain" into <username> and domain."""
3892 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003893 # distinguishable from sub-domains. But we do know typical domains:
3894 if identity.endswith('.chromium.org'):
3895 domain = 'chromium.org'
3896 username = identity[:-len('.chromium.org')]
3897 else:
3898 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003899 if username.startswith('git-'):
3900 username = username[len('git-'):]
3901 return username, domain
3902
3903 def _get_usernames_of_domain(self, domain):
3904 """Returns list of usernames referenced by .gitcookies in a given domain."""
3905 identities_by_domain = {}
3906 for _, identity, _ in self.get_hosts_with_creds():
3907 username, domain = self._parse_identity(identity)
3908 identities_by_domain.setdefault(domain, []).append(username)
3909 return identities_by_domain.get(domain)
3910
3911 def _canonical_git_googlesource_host(self, host):
3912 """Normalizes Gerrit hosts (with '-review') to Git host."""
3913 assert host.endswith(self._GOOGLESOURCE)
3914 # Prefix doesn't include '.' at the end.
3915 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3916 if prefix.endswith('-review'):
3917 prefix = prefix[:-len('-review')]
3918 return prefix + '.' + self._GOOGLESOURCE
3919
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003920 def _canonical_gerrit_googlesource_host(self, host):
3921 git_host = self._canonical_git_googlesource_host(host)
3922 prefix = git_host.split('.', 1)[0]
3923 return prefix + '-review.' + self._GOOGLESOURCE
3924
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003925 def _get_counterpart_host(self, host):
3926 assert host.endswith(self._GOOGLESOURCE)
3927 git = self._canonical_git_googlesource_host(host)
3928 gerrit = self._canonical_gerrit_googlesource_host(git)
3929 return git if gerrit == host else gerrit
3930
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003931 def has_generic_host(self):
3932 """Returns whether generic .googlesource.com has been configured.
3933
3934 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3935 """
3936 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3937 if host == '.' + self._GOOGLESOURCE:
3938 return True
3939 return False
3940
3941 def _get_git_gerrit_identity_pairs(self):
3942 """Returns map from canonic host to pair of identities (Git, Gerrit).
3943
3944 One of identities might be None, meaning not configured.
3945 """
3946 host_to_identity_pairs = {}
3947 for host, identity, _ in self.get_hosts_with_creds():
3948 canonical = self._canonical_git_googlesource_host(host)
3949 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3950 idx = 0 if canonical == host else 1
3951 pair[idx] = identity
3952 return host_to_identity_pairs
3953
3954 def get_partially_configured_hosts(self):
3955 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003956 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3957 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3958 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003959
3960 def get_conflicting_hosts(self):
3961 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003962 host
3963 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003964 if None not in (i1, i2) and i1 != i2)
3965
3966 def get_duplicated_hosts(self):
3967 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3968 return set(host for host, count in counters.iteritems() if count > 1)
3969
3970 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3971 'chromium.googlesource.com': 'chromium.org',
3972 'chrome-internal.googlesource.com': 'google.com',
3973 }
3974
3975 def get_hosts_with_wrong_identities(self):
3976 """Finds hosts which **likely** reference wrong identities.
3977
3978 Note: skips hosts which have conflicting identities for Git and Gerrit.
3979 """
3980 hosts = set()
3981 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3982 pair = self._get_git_gerrit_identity_pairs().get(host)
3983 if pair and pair[0] == pair[1]:
3984 _, domain = self._parse_identity(pair[0])
3985 if domain != expected:
3986 hosts.add(host)
3987 return hosts
3988
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003989 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003990 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003991 hosts = sorted(hosts)
3992 assert hosts
3993 if extra_column_func is None:
3994 extras = [''] * len(hosts)
3995 else:
3996 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003997 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3998 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003999 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004000 lines.append(tmpl % he)
4001 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004002
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004003 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004004 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004005 yield ('.googlesource.com wildcard record detected',
4006 ['Chrome Infrastructure team recommends to list full host names '
4007 'explicitly.'],
4008 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004009
4010 dups = self.get_duplicated_hosts()
4011 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004012 yield ('The following hosts were defined twice',
4013 self._format_hosts(dups),
4014 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004015
4016 partial = self.get_partially_configured_hosts()
4017 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004018 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4019 'These hosts are missing',
4020 self._format_hosts(partial, lambda host: 'but %s defined' %
4021 self._get_counterpart_host(host)),
4022 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004023
4024 conflicting = self.get_conflicting_hosts()
4025 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004026 yield ('The following Git hosts have differing credentials from their '
4027 'Gerrit counterparts',
4028 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4029 tuple(self._get_git_gerrit_identity_pairs()[host])),
4030 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004031
4032 wrong = self.get_hosts_with_wrong_identities()
4033 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004034 yield ('These hosts likely use wrong identity',
4035 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4036 (self._get_git_gerrit_identity_pairs()[host][0],
4037 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4038 wrong)
4039
4040 def find_and_report_problems(self):
4041 """Returns True if there was at least one problem, else False."""
4042 found = False
4043 bad_hosts = set()
4044 for title, sublines, hosts in self._find_problems():
4045 if not found:
4046 found = True
4047 print('\n\n.gitcookies problem report:\n')
4048 bad_hosts.update(hosts or [])
4049 print(' %s%s' % (title , (':' if sublines else '')))
4050 if sublines:
4051 print()
4052 print(' %s' % '\n '.join(sublines))
4053 print()
4054
4055 if bad_hosts:
4056 assert found
4057 print(' You can manually remove corresponding lines in your %s file and '
4058 'visit the following URLs with correct account to generate '
4059 'correct credential lines:\n' %
4060 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4061 print(' %s' % '\n '.join(sorted(set(
4062 gerrit_util.CookiesAuthenticator().get_new_password_url(
4063 self._canonical_git_googlesource_host(host))
4064 for host in bad_hosts
4065 ))))
4066 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004067
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004068
4069def CMDcreds_check(parser, args):
4070 """Checks credentials and suggests changes."""
4071 _, _ = parser.parse_args(args)
4072
4073 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004074 DieWithError(
4075 'This command is not designed for GCE, are you on a bot?\n'
4076 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004077
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004078 checker = _GitCookiesChecker()
4079 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004080
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004081 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004082 checker.print_current_creds(include_netrc=True)
4083
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004084 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004085 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004086 return 0
4087 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004088
4089
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004090@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004091def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004092 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004094 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004095 # TODO(tandrii): remove this once we switch to Gerrit.
4096 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004097 parser.add_option('--activate-update', action='store_true',
4098 help='activate auto-updating [rietveld] section in '
4099 '.git/config')
4100 parser.add_option('--deactivate-update', action='store_true',
4101 help='deactivate auto-updating [rietveld] section in '
4102 '.git/config')
4103 options, args = parser.parse_args(args)
4104
4105 if options.deactivate_update:
4106 RunGit(['config', 'rietveld.autoupdate', 'false'])
4107 return
4108
4109 if options.activate_update:
4110 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4111 return
4112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004113 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004114 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115 return 0
4116
4117 url = args[0]
4118 if not url.endswith('codereview.settings'):
4119 url = os.path.join(url, 'codereview.settings')
4120
4121 # Load code review settings and download hooks (if available).
4122 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4123 return 0
4124
4125
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004126def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004127 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004128 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4129 branch = ShortBranchName(branchref)
4130 _, args = parser.parse_args(args)
4131 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004132 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004133 return RunGit(['config', 'branch.%s.base-url' % branch],
4134 error_ok=False).strip()
4135 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004137 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4138 error_ok=False).strip()
4139
4140
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004141def color_for_status(status):
4142 """Maps a Changelist status to color, for CMDstatus and other tools."""
4143 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004144 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004145 'waiting': Fore.BLUE,
4146 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004147 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004148 'lgtm': Fore.GREEN,
4149 'commit': Fore.MAGENTA,
4150 'closed': Fore.CYAN,
4151 'error': Fore.WHITE,
4152 }.get(status, Fore.WHITE)
4153
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004154
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004155def get_cl_statuses(changes, fine_grained, max_processes=None):
4156 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004157
4158 If fine_grained is true, this will fetch CL statuses from the server.
4159 Otherwise, simply indicate if there's a matching url for the given branches.
4160
4161 If max_processes is specified, it is used as the maximum number of processes
4162 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4163 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004164
4165 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004166 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004167 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004168 upload.verbosity = 0
4169
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004170 if not changes:
4171 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004172
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004173 if not fine_grained:
4174 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004175 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004176 for cl in changes:
4177 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004178 return
4179
4180 # First, sort out authentication issues.
4181 logging.debug('ensuring credentials exist')
4182 for cl in changes:
4183 cl.EnsureAuthenticated(force=False, refresh=True)
4184
4185 def fetch(cl):
4186 try:
4187 return (cl, cl.GetStatus())
4188 except:
4189 # See http://crbug.com/629863.
4190 logging.exception('failed to fetch status for %s:', cl)
4191 raise
4192
4193 threads_count = len(changes)
4194 if max_processes:
4195 threads_count = max(1, min(threads_count, max_processes))
4196 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4197
4198 pool = ThreadPool(threads_count)
4199 fetched_cls = set()
4200 try:
4201 it = pool.imap_unordered(fetch, changes).__iter__()
4202 while True:
4203 try:
4204 cl, status = it.next(timeout=5)
4205 except multiprocessing.TimeoutError:
4206 break
4207 fetched_cls.add(cl)
4208 yield cl, status
4209 finally:
4210 pool.close()
4211
4212 # Add any branches that failed to fetch.
4213 for cl in set(changes) - fetched_cls:
4214 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004215
rmistry@google.com2dd99862015-06-22 12:22:18 +00004216
4217def upload_branch_deps(cl, args):
4218 """Uploads CLs of local branches that are dependents of the current branch.
4219
4220 If the local branch dependency tree looks like:
4221 test1 -> test2.1 -> test3.1
4222 -> test3.2
4223 -> test2.2 -> test3.3
4224
4225 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4226 run on the dependent branches in this order:
4227 test2.1, test3.1, test3.2, test2.2, test3.3
4228
4229 Note: This function does not rebase your local dependent branches. Use it when
4230 you make a change to the parent branch that will not conflict with its
4231 dependent branches, and you would like their dependencies updated in
4232 Rietveld.
4233 """
4234 if git_common.is_dirty_git_tree('upload-branch-deps'):
4235 return 1
4236
4237 root_branch = cl.GetBranch()
4238 if root_branch is None:
4239 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4240 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004241 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004242 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4243 'patchset dependencies without an uploaded CL.')
4244
4245 branches = RunGit(['for-each-ref',
4246 '--format=%(refname:short) %(upstream:short)',
4247 'refs/heads'])
4248 if not branches:
4249 print('No local branches found.')
4250 return 0
4251
4252 # Create a dictionary of all local branches to the branches that are dependent
4253 # on it.
4254 tracked_to_dependents = collections.defaultdict(list)
4255 for b in branches.splitlines():
4256 tokens = b.split()
4257 if len(tokens) == 2:
4258 branch_name, tracked = tokens
4259 tracked_to_dependents[tracked].append(branch_name)
4260
vapiera7fbd5a2016-06-16 09:17:49 -07004261 print()
4262 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004263 dependents = []
4264 def traverse_dependents_preorder(branch, padding=''):
4265 dependents_to_process = tracked_to_dependents.get(branch, [])
4266 padding += ' '
4267 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004268 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004269 dependents.append(dependent)
4270 traverse_dependents_preorder(dependent, padding)
4271 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004272 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004273
4274 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004275 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004276 return 0
4277
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004278 confirm_or_exit('This command will checkout all dependent branches and run '
4279 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004280
andybons@chromium.org962f9462016-02-03 20:00:42 +00004281 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004282 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004283 args.extend(['-t', 'Updated patchset dependency'])
4284
rmistry@google.com2dd99862015-06-22 12:22:18 +00004285 # Record all dependents that failed to upload.
4286 failures = {}
4287 # Go through all dependents, checkout the branch and upload.
4288 try:
4289 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004290 print()
4291 print('--------------------------------------')
4292 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004293 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004294 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004295 try:
4296 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004298 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004299 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004300 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004301 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004302 finally:
4303 # Swap back to the original root branch.
4304 RunGit(['checkout', '-q', root_branch])
4305
vapiera7fbd5a2016-06-16 09:17:49 -07004306 print()
4307 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004308 for dependent_branch in dependents:
4309 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print(' %s : %s' % (dependent_branch, upload_status))
4311 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004312
4313 return 0
4314
4315
kmarshall3bff56b2016-06-06 18:31:47 -07004316def CMDarchive(parser, args):
4317 """Archives and deletes branches associated with closed changelists."""
4318 parser.add_option(
4319 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004320 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004321 parser.add_option(
4322 '-f', '--force', action='store_true',
4323 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004324 parser.add_option(
4325 '-d', '--dry-run', action='store_true',
4326 help='Skip the branch tagging and removal steps.')
4327 parser.add_option(
4328 '-t', '--notags', action='store_true',
4329 help='Do not tag archived branches. '
4330 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004331
4332 auth.add_auth_options(parser)
4333 options, args = parser.parse_args(args)
4334 if args:
4335 parser.error('Unsupported args: %s' % ' '.join(args))
4336 auth_config = auth.extract_auth_config_from_options(options)
4337
4338 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4339 if not branches:
4340 return 0
4341
vapiera7fbd5a2016-06-16 09:17:49 -07004342 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004343 changes = [Changelist(branchref=b, auth_config=auth_config)
4344 for b in branches.splitlines()]
4345 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4346 statuses = get_cl_statuses(changes,
4347 fine_grained=True,
4348 max_processes=options.maxjobs)
4349 proposal = [(cl.GetBranch(),
4350 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4351 for cl, status in statuses
4352 if status == 'closed']
4353 proposal.sort()
4354
4355 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004357 return 0
4358
4359 current_branch = GetCurrentBranch()
4360
vapiera7fbd5a2016-06-16 09:17:49 -07004361 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004362 if options.notags:
4363 for next_item in proposal:
4364 print(' ' + next_item[0])
4365 else:
4366 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4367 for next_item in proposal:
4368 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004369
kmarshall9249e012016-08-23 12:02:16 -07004370 # Quit now on precondition failure or if instructed by the user, either
4371 # via an interactive prompt or by command line flags.
4372 if options.dry_run:
4373 print('\nNo changes were made (dry run).\n')
4374 return 0
4375 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004376 print('You are currently on a branch \'%s\' which is associated with a '
4377 'closed codereview issue, so archive cannot proceed. Please '
4378 'checkout another branch and run this command again.' %
4379 current_branch)
4380 return 1
kmarshall9249e012016-08-23 12:02:16 -07004381 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004382 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4383 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004384 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004385 return 1
4386
4387 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004388 if not options.notags:
4389 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004390 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004391
vapiera7fbd5a2016-06-16 09:17:49 -07004392 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004393
4394 return 0
4395
4396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004398 """Show status of changelists.
4399
4400 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004401 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004402 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004403 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004404 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004405 - Magenta in the commit queue
4406 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004407 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004408
4409 Also see 'git cl comments'.
4410 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004412 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004413 parser.add_option('-f', '--fast', action='store_true',
4414 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004415 parser.add_option(
4416 '-j', '--maxjobs', action='store', type=int,
4417 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004418
4419 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004420 _add_codereview_issue_select_options(
4421 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004422 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004423 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004424 if args:
4425 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004426 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427
iannuccie53c9352016-08-17 14:40:40 -07004428 if options.issue is not None and not options.field:
4429 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004432 cl = Changelist(auth_config=auth_config, issue=options.issue,
4433 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004434 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004435 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436 elif options.field == 'id':
4437 issueid = cl.GetIssue()
4438 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004439 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004440 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004441 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004442 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004443 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004444 elif options.field == 'status':
4445 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446 elif options.field == 'url':
4447 url = cl.GetIssueURL()
4448 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004449 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004450 return 0
4451
4452 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4453 if not branches:
4454 print('No local branch found.')
4455 return 0
4456
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004457 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004458 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004459 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004460 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004461 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004462 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004463 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004464
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004465 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004466 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4467 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4468 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004469 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004470 c, status = output.next()
4471 branch_statuses[c.GetBranch()] = status
4472 status = branch_statuses.pop(branch)
4473 url = cl.GetIssueURL()
4474 if url and (not status or status == 'error'):
4475 # The issue probably doesn't exist anymore.
4476 url += ' (broken)'
4477
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004478 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004479 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004480 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004481 color = ''
4482 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004483 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004484 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004485 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004486 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004487
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004488
4489 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004490 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004491 print('Current branch: %s' % branch)
4492 for cl in changes:
4493 if cl.GetBranch() == branch:
4494 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004495 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004496 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004497 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004498 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004499 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004500 print('Issue description:')
4501 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004502 return 0
4503
4504
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004505def colorize_CMDstatus_doc():
4506 """To be called once in main() to add colors to git cl status help."""
4507 colors = [i for i in dir(Fore) if i[0].isupper()]
4508
4509 def colorize_line(line):
4510 for color in colors:
4511 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004512 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004513 indent = len(line) - len(line.lstrip(' ')) + 1
4514 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4515 return line
4516
4517 lines = CMDstatus.__doc__.splitlines()
4518 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4519
4520
phajdan.jre328cf92016-08-22 04:12:17 -07004521def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004522 if path == '-':
4523 json.dump(contents, sys.stdout)
4524 else:
4525 with open(path, 'w') as f:
4526 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004527
4528
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004529@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004531 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532
4533 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004534 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004535 parser.add_option('-r', '--reverse', action='store_true',
4536 help='Lookup the branch(es) for the specified issues. If '
4537 'no issues are specified, all branches with mapped '
4538 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004539 parser.add_option('--json',
4540 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004541 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004542 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004543 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004544
dnj@chromium.org406c4402015-03-03 17:22:28 +00004545 if options.reverse:
4546 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004547 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004548 # Reverse issue lookup.
4549 issue_branch_map = {}
4550 for branch in branches:
4551 cl = Changelist(branchref=branch)
4552 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4553 if not args:
4554 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004555 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004556 for issue in args:
4557 if not issue:
4558 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004559 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004560 print('Branch for issue number %s: %s' % (
4561 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004562 if options.json:
4563 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004564 return 0
4565
4566 if len(args) > 0:
4567 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4568 if not issue.valid:
4569 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4570 'or no argument to list it.\n'
4571 'Maybe you want to run git cl status?')
4572 cl = Changelist(codereview=issue.codereview)
4573 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004574 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004575 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004576 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4577 if options.json:
4578 write_json(options.json, {
4579 'issue': cl.GetIssue(),
4580 'issue_url': cl.GetIssueURL(),
4581 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582 return 0
4583
4584
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004585def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004586 """Shows or posts review comments for any changelist."""
4587 parser.add_option('-a', '--add-comment', dest='comment',
4588 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004589 parser.add_option('-i', '--issue', dest='issue',
4590 help='review issue id (defaults to current issue). '
4591 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004592 parser.add_option('-m', '--machine-readable', dest='readable',
4593 action='store_false', default=True,
4594 help='output comments in a format compatible with '
4595 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004596 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004597 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004598 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004599 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004600 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004601 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004602 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004603
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004604 issue = None
4605 if options.issue:
4606 try:
4607 issue = int(options.issue)
4608 except ValueError:
4609 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004610 if not options.forced_codereview:
4611 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004612
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004613 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004614 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004615 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004616
4617 if options.comment:
4618 cl.AddComment(options.comment)
4619 return 0
4620
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004621 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4622 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004623 for comment in summary:
4624 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004625 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004626 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004627 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004628 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004629 color = Fore.MAGENTA
4630 else:
4631 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004632 print('\n%s%s %s%s\n%s' % (
4633 color,
4634 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4635 comment.sender,
4636 Fore.RESET,
4637 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4638
smut@google.comc85ac942015-09-15 16:34:43 +00004639 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004640 def pre_serialize(c):
4641 dct = c.__dict__.copy()
4642 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4643 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004644 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004645 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004646 return 0
4647
4648
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004649@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004650def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004651 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004652 parser.add_option('-d', '--display', action='store_true',
4653 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004654 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004655 help='New description to set for this issue (- for stdin, '
4656 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004657 parser.add_option('-f', '--force', action='store_true',
4658 help='Delete any unpublished Gerrit edits for this issue '
4659 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004660
4661 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004662 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004663 options, args = parser.parse_args(args)
4664 _process_codereview_select_options(parser, options)
4665
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004666 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004667 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004668 target_issue_arg = ParseIssueNumberArgument(args[0],
4669 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004670 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004671 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004672
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004673 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004674
martiniss6eda05f2016-06-30 10:18:35 -07004675 kwargs = {
4676 'auth_config': auth_config,
4677 'codereview': options.forced_codereview,
4678 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004679 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004680 if target_issue_arg:
4681 kwargs['issue'] = target_issue_arg.issue
4682 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004683 if target_issue_arg.codereview and not options.forced_codereview:
4684 detected_codereview_from_url = True
4685 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004686
4687 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004688 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004689 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004690 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004691
4692 if detected_codereview_from_url:
4693 logging.info('canonical issue/change URL: %s (type: %s)\n',
4694 cl.GetIssueURL(), target_issue_arg.codereview)
4695
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004696 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004697
smut@google.com34fb6b12015-07-13 20:03:26 +00004698 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004699 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004700 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004701
4702 if options.new_description:
4703 text = options.new_description
4704 if text == '-':
4705 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004706 elif text == '+':
4707 base_branch = cl.GetCommonAncestorWithUpstream()
4708 change = cl.GetChange(base_branch, None, local_description=True)
4709 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004710
4711 description.set_description(text)
4712 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004713 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004714
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004715 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004716 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004717 return 0
4718
4719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004720def CreateDescriptionFromLog(args):
4721 """Pulls out the commit log to use as a base for the CL description."""
4722 log_args = []
4723 if len(args) == 1 and not args[0].endswith('.'):
4724 log_args = [args[0] + '..']
4725 elif len(args) == 1 and args[0].endswith('...'):
4726 log_args = [args[0][:-1]]
4727 elif len(args) == 2:
4728 log_args = [args[0] + '..' + args[1]]
4729 else:
4730 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004731 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004732
4733
thestig@chromium.org44202a22014-03-11 19:22:18 +00004734def CMDlint(parser, args):
4735 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004736 parser.add_option('--filter', action='append', metavar='-x,+y',
4737 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004738 auth.add_auth_options(parser)
4739 options, args = parser.parse_args(args)
4740 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004741
4742 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004743 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004744 try:
4745 import cpplint
4746 import cpplint_chromium
4747 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004748 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004749 return 1
4750
4751 # Change the current working directory before calling lint so that it
4752 # shows the correct base.
4753 previous_cwd = os.getcwd()
4754 os.chdir(settings.GetRoot())
4755 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004756 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004757 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4758 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004759 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004760 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004761 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004762
4763 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004764 command = args + files
4765 if options.filter:
4766 command = ['--filter=' + ','.join(options.filter)] + command
4767 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004768
4769 white_regex = re.compile(settings.GetLintRegex())
4770 black_regex = re.compile(settings.GetLintIgnoreRegex())
4771 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4772 for filename in filenames:
4773 if white_regex.match(filename):
4774 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004775 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004776 else:
4777 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4778 extra_check_functions)
4779 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004780 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004781 finally:
4782 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004783 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004784 if cpplint._cpplint_state.error_count != 0:
4785 return 1
4786 return 0
4787
4788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004790 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004791 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004792 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004793 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004794 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004795 parser.add_option('--all', action='store_true',
4796 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004797 parser.add_option('--parallel', action='store_true',
4798 help='Run all tests specified by input_api.RunTests in all '
4799 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004800 auth.add_auth_options(parser)
4801 options, args = parser.parse_args(args)
4802 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004803
sbc@chromium.org71437c02015-04-09 19:29:40 +00004804 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004805 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004806 return 1
4807
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004808 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004809 if args:
4810 base_branch = args[0]
4811 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004812 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004813 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004814
Aaron Gable8076c282017-11-29 14:39:41 -08004815 if options.all:
4816 base_change = cl.GetChange(base_branch, None)
4817 files = [('M', f) for f in base_change.AllFiles()]
4818 change = presubmit_support.GitChange(
4819 base_change.Name(),
4820 base_change.FullDescriptionText(),
4821 base_change.RepositoryRoot(),
4822 files,
4823 base_change.issue,
4824 base_change.patchset,
4825 base_change.author_email,
4826 base_change._upstream)
4827 else:
4828 change = cl.GetChange(base_branch, None)
4829
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004830 cl.RunHook(
4831 committing=not options.upload,
4832 may_prompt=False,
4833 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004834 change=change,
4835 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004836 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004837
4838
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004839def GenerateGerritChangeId(message):
4840 """Returns Ixxxxxx...xxx change id.
4841
4842 Works the same way as
4843 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4844 but can be called on demand on all platforms.
4845
4846 The basic idea is to generate git hash of a state of the tree, original commit
4847 message, author/committer info and timestamps.
4848 """
4849 lines = []
4850 tree_hash = RunGitSilent(['write-tree'])
4851 lines.append('tree %s' % tree_hash.strip())
4852 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4853 if code == 0:
4854 lines.append('parent %s' % parent.strip())
4855 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4856 lines.append('author %s' % author.strip())
4857 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4858 lines.append('committer %s' % committer.strip())
4859 lines.append('')
4860 # Note: Gerrit's commit-hook actually cleans message of some lines and
4861 # whitespace. This code is not doing this, but it clearly won't decrease
4862 # entropy.
4863 lines.append(message)
4864 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4865 stdin='\n'.join(lines))
4866 return 'I%s' % change_hash.strip()
4867
4868
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004869def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004870 """Computes the remote branch ref to use for the CL.
4871
4872 Args:
4873 remote (str): The git remote for the CL.
4874 remote_branch (str): The git remote branch for the CL.
4875 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004876 """
4877 if not (remote and remote_branch):
4878 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004879
wittman@chromium.org455dc922015-01-26 20:15:50 +00004880 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004881 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004882 # refs, which are then translated into the remote full symbolic refs
4883 # below.
4884 if '/' not in target_branch:
4885 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4886 else:
4887 prefix_replacements = (
4888 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4889 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4890 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4891 )
4892 match = None
4893 for regex, replacement in prefix_replacements:
4894 match = re.search(regex, target_branch)
4895 if match:
4896 remote_branch = target_branch.replace(match.group(0), replacement)
4897 break
4898 if not match:
4899 # This is a branch path but not one we recognize; use as-is.
4900 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004901 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4902 # Handle the refs that need to land in different refs.
4903 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004904
wittman@chromium.org455dc922015-01-26 20:15:50 +00004905 # Create the true path to the remote branch.
4906 # Does the following translation:
4907 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4908 # * refs/remotes/origin/master -> refs/heads/master
4909 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4910 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4911 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4912 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4913 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4914 'refs/heads/')
4915 elif remote_branch.startswith('refs/remotes/branch-heads'):
4916 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004917
wittman@chromium.org455dc922015-01-26 20:15:50 +00004918 return remote_branch
4919
4920
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004921def cleanup_list(l):
4922 """Fixes a list so that comma separated items are put as individual items.
4923
4924 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4925 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4926 """
4927 items = sum((i.split(',') for i in l), [])
4928 stripped_items = (i.strip() for i in items)
4929 return sorted(filter(None, stripped_items))
4930
4931
Aaron Gable4db38df2017-11-03 14:59:07 -07004932@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004933def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004934 """Uploads the current changelist to codereview.
4935
4936 Can skip dependency patchset uploads for a branch by running:
4937 git config branch.branch_name.skip-deps-uploads True
4938 To unset run:
4939 git config --unset branch.branch_name.skip-deps-uploads
4940 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004941
4942 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4943 a bug number, this bug number is automatically populated in the CL
4944 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004945
4946 If subject contains text in square brackets or has "<text>: " prefix, such
4947 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4948 [git-cl] add support for hashtags
4949 Foo bar: implement foo
4950 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004951 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004952 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4953 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004954 parser.add_option('--bypass-watchlists', action='store_true',
4955 dest='bypass_watchlists',
4956 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004957 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004958 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004959 parser.add_option('--message', '-m', dest='message',
4960 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004961 parser.add_option('-b', '--bug',
4962 help='pre-populate the bug number(s) for this issue. '
4963 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004964 parser.add_option('--message-file', dest='message_file',
4965 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004966 parser.add_option('--title', '-t', dest='title',
4967 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004968 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004969 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004970 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004971 parser.add_option('--tbrs',
4972 action='append', default=[],
4973 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004974 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004975 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004976 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004977 parser.add_option('--hashtag', dest='hashtags',
4978 action='append', default=[],
4979 help=('Gerrit hashtag for new CL; '
4980 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004981 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004982 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004983 parser.add_option('--emulate_svn_auto_props',
4984 '--emulate-svn-auto-props',
4985 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004986 dest="emulate_svn_auto_props",
4987 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004988 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004989 help='tell the commit queue to commit this patchset; '
4990 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004991 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004992 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004993 metavar='TARGET',
4994 help='Apply CL to remote ref TARGET. ' +
4995 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004996 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004997 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004998 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004999 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005000 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005001 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005002 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5003 const='TBR', help='add a set of OWNERS to TBR')
5004 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5005 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005006 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5007 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005008 help='Send the patchset to do a CQ dry run right after '
5009 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005010 parser.add_option('--dependencies', action='store_true',
5011 help='Uploads CLs of all the local branches that depend on '
5012 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005013 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5014 help='Sends your change to the CQ after an approval. Only '
5015 'works on repos that have the Auto-Submit label '
5016 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005017 parser.add_option('--parallel', action='store_true',
5018 help='Run all tests specified by input_api.RunTests in all '
5019 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005020
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005021 # TODO: remove Rietveld flags
5022 parser.add_option('--private', action='store_true',
5023 help='set the review private (rietveld only)')
5024 parser.add_option('--email', default=None,
5025 help='email address to use to connect to Rietveld')
5026
rmistry@google.com2dd99862015-06-22 12:22:18 +00005027 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005028 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005029 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005030 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005031 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005032 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005033
sbc@chromium.org71437c02015-04-09 19:29:40 +00005034 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005035 return 1
5036
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005037 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005038 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005039 options.cc = cleanup_list(options.cc)
5040
tandriib80458a2016-06-23 12:20:07 -07005041 if options.message_file:
5042 if options.message:
5043 parser.error('only one of --message and --message-file allowed.')
5044 options.message = gclient_utils.FileRead(options.message_file)
5045 options.message_file = None
5046
tandrii4d0545a2016-07-06 03:56:49 -07005047 if options.cq_dry_run and options.use_commit_queue:
5048 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5049
Aaron Gableedbc4132017-09-11 13:22:28 -07005050 if options.use_commit_queue:
5051 options.send_mail = True
5052
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005053 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5054 settings.GetIsGerrit()
5055
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005056 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005057 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005058
5059
Francois Dorayd42c6812017-05-30 15:10:20 -04005060@subcommand.usage('--description=<description file>')
5061def CMDsplit(parser, args):
5062 """Splits a branch into smaller branches and uploads CLs.
5063
5064 Creates a branch and uploads a CL for each group of files modified in the
5065 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005066 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005067 the shared OWNERS file.
5068 """
5069 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005070 help="A text file containing a CL description in which "
5071 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005072 parser.add_option("-c", "--comment", dest="comment_file",
5073 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005074 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5075 default=False,
5076 help="List the files and reviewers for each CL that would "
5077 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005078 options, _ = parser.parse_args(args)
5079
5080 if not options.description_file:
5081 parser.error('No --description flag specified.')
5082
5083 def WrappedCMDupload(args):
5084 return CMDupload(OptionParser(), args)
5085
5086 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005087 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005088
5089
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005090@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005091def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005092 """DEPRECATED: Used to commit the current changelist via git-svn."""
5093 message = ('git-cl no longer supports committing to SVN repositories via '
5094 'git-svn. You probably want to use `git cl land` instead.')
5095 print(message)
5096 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005097
5098
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005099# Two special branches used by git cl land.
5100MERGE_BRANCH = 'git-cl-commit'
5101CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5102
5103
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005104@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005105def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005106 """Commits the current changelist via git.
5107
5108 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5109 upstream and closes the issue automatically and atomically.
5110
5111 Otherwise (in case of Rietveld):
5112 Squashes branch into a single commit.
5113 Updates commit message with metadata (e.g. pointer to review).
5114 Pushes the code upstream.
5115 Updates review and closes.
5116 """
5117 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5118 help='bypass upload presubmit hook')
5119 parser.add_option('-m', dest='message',
5120 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005121 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005122 help="force yes to questions (don't prompt)")
5123 parser.add_option('-c', dest='contributor',
5124 help="external contributor for patch (appended to " +
5125 "description and used as author for git). Should be " +
5126 "formatted as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005127 auth.add_auth_options(parser)
5128 (options, args) = parser.parse_args(args)
5129 auth_config = auth.extract_auth_config_from_options(options)
5130
5131 cl = Changelist(auth_config=auth_config)
5132
Robert Iannucci2e73d432018-03-14 01:10:47 -07005133 if not cl.IsGerrit():
5134 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005135
Robert Iannucci2e73d432018-03-14 01:10:47 -07005136 if options.message:
5137 # This could be implemented, but it requires sending a new patch to
5138 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5139 # Besides, Gerrit has the ability to change the commit message on submit
5140 # automatically, thus there is no need to support this option (so far?).
5141 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005142 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005143 parser.error(
5144 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5145 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5146 'the contributor\'s "name <email>". If you can\'t upload such a '
5147 'commit for review, contact your repository admin and request'
5148 '"Forge-Author" permission.')
5149 if not cl.GetIssue():
5150 DieWithError('You must upload the change first to Gerrit.\n'
5151 ' If you would rather have `git cl land` upload '
5152 'automatically for you, see http://crbug.com/642759')
5153 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5154 options.verbose)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005155
5156
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005157def PushToGitWithAutoRebase(remote, branch, original_description,
5158 git_numberer_enabled, max_attempts=3):
5159 """Pushes current HEAD commit on top of remote's branch.
5160
5161 Attempts to fetch and autorebase on push failures.
5162 Adds git number footers on the fly.
5163
5164 Returns integer code from last command.
5165 """
5166 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5167 code = 0
5168 attempts_left = max_attempts
5169 while attempts_left:
5170 attempts_left -= 1
5171 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5172
5173 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5174 # If fetch fails, retry.
5175 print('Fetching %s/%s...' % (remote, branch))
5176 code, out = RunGitWithCode(
5177 ['retry', 'fetch', remote,
5178 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5179 if code:
5180 print('Fetch failed with exit code %d.' % code)
5181 print(out.strip())
5182 continue
5183
5184 print('Cherry-picking commit on top of latest %s' % branch)
5185 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5186 suppress_stderr=True)
5187 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5188 code, out = RunGitWithCode(['cherry-pick', cherry])
5189 if code:
5190 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5191 'the following files have merge conflicts:' %
5192 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005193 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5194 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005195 print('Please rebase your patch and try again.')
5196 RunGitWithCode(['cherry-pick', '--abort'])
5197 break
5198
5199 commit_desc = ChangeDescription(original_description)
5200 if git_numberer_enabled:
5201 logging.debug('Adding git number footers')
5202 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5203 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5204 branch)
5205 # Ensure timestamps are monotonically increasing.
5206 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5207 _get_committer_timestamp('HEAD'))
5208 _git_amend_head(commit_desc.description, timestamp)
5209
5210 code, out = RunGitWithCode(
5211 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5212 print(out)
5213 if code == 0:
5214 break
5215 if IsFatalPushFailure(out):
5216 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005217 'user.email are correct and you have push access to the repo.\n'
5218 'Hint: run command below to diangose common Git/Gerrit credential '
5219 'problems:\n'
5220 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005221 break
5222 return code
5223
5224
5225def IsFatalPushFailure(push_stdout):
5226 """True if retrying push won't help."""
5227 return '(prohibited by Gerrit)' in push_stdout
5228
5229
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005230@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005231def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005232 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005233 parser.add_option('-b', dest='newbranch',
5234 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005235 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005236 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005237 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005238 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005239 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005240 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005241 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005242 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005243 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005244 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005245
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005246
5247 group = optparse.OptionGroup(
5248 parser,
5249 'Options for continuing work on the current issue uploaded from a '
5250 'different clone (e.g. different machine). Must be used independently '
5251 'from the other options. No issue number should be specified, and the '
5252 'branch must have an issue number associated with it')
5253 group.add_option('--reapply', action='store_true', dest='reapply',
5254 help='Reset the branch and reapply the issue.\n'
5255 'CAUTION: This will undo any local changes in this '
5256 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005257
5258 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005259 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005260 parser.add_option_group(group)
5261
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005262 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005263 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005264 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005265 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005266 auth_config = auth.extract_auth_config_from_options(options)
5267
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005268 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005269 if options.newbranch:
5270 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005271 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005272 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005273
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005274 cl = Changelist(auth_config=auth_config,
5275 codereview=options.forced_codereview)
5276 if not cl.GetIssue():
5277 parser.error('current branch must have an associated issue')
5278
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005279 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005280 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005281 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005282
5283 RunGit(['reset', '--hard', upstream])
5284 if options.pull:
5285 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005286
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005287 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5288 options.directory)
5289
5290 if len(args) != 1 or not args[0]:
5291 parser.error('Must specify issue number or url')
5292
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005293 target_issue_arg = ParseIssueNumberArgument(args[0],
5294 options.forced_codereview)
5295 if not target_issue_arg.valid:
5296 parser.error('invalid codereview url or CL id')
5297
5298 cl_kwargs = {
5299 'auth_config': auth_config,
5300 'codereview_host': target_issue_arg.hostname,
5301 'codereview': options.forced_codereview,
5302 }
5303 detected_codereview_from_url = False
5304 if target_issue_arg.codereview and not options.forced_codereview:
5305 detected_codereview_from_url = True
5306 cl_kwargs['codereview'] = target_issue_arg.codereview
5307 cl_kwargs['issue'] = target_issue_arg.issue
5308
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005309 # We don't want uncommitted changes mixed up with the patch.
5310 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005311 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005312
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005313 if options.newbranch:
5314 if options.force:
5315 RunGit(['branch', '-D', options.newbranch],
5316 stderr=subprocess2.PIPE, error_ok=True)
5317 RunGit(['new-branch', options.newbranch])
5318
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005319 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005320
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005321 if cl.IsGerrit():
5322 if options.reject:
5323 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005324 if options.directory:
5325 parser.error('--directory is not supported with Gerrit codereview.')
5326
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005327 if detected_codereview_from_url:
5328 print('canonical issue/change URL: %s (type: %s)\n' %
5329 (cl.GetIssueURL(), target_issue_arg.codereview))
5330
5331 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005332 options.nocommit, options.directory,
5333 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005334
5335
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005336def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005337 """Fetches the tree status and returns either 'open', 'closed',
5338 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005339 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005340 if url:
5341 status = urllib2.urlopen(url).read().lower()
5342 if status.find('closed') != -1 or status == '0':
5343 return 'closed'
5344 elif status.find('open') != -1 or status == '1':
5345 return 'open'
5346 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005347 return 'unset'
5348
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005350def GetTreeStatusReason():
5351 """Fetches the tree status from a json url and returns the message
5352 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005353 url = settings.GetTreeStatusUrl()
5354 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005355 connection = urllib2.urlopen(json_url)
5356 status = json.loads(connection.read())
5357 connection.close()
5358 return status['message']
5359
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005360
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005361def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005362 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005363 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005364 status = GetTreeStatus()
5365 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005367 return 2
5368
vapiera7fbd5a2016-06-16 09:17:49 -07005369 print('The tree is %s' % status)
5370 print()
5371 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005372 if status != 'open':
5373 return 1
5374 return 0
5375
5376
maruel@chromium.org15192402012-09-06 12:38:29 +00005377def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005378 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005379 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005380 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005381 '-b', '--bot', action='append',
5382 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5383 'times to specify multiple builders. ex: '
5384 '"-b win_rel -b win_layout". See '
5385 'the try server waterfall for the builders name and the tests '
5386 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005387 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005388 '-B', '--bucket', default='',
5389 help=('Buildbucket bucket to send the try requests.'))
5390 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005391 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005392 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005393 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005394 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005395 help='Revision to use for the try job; default: the revision will '
5396 'be determined by the try recipe that builder runs, which usually '
5397 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005398 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005399 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005400 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005401 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005402 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005403 '--category', default='git_cl_try', help='Specify custom build category.')
5404 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005405 '--project',
5406 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005407 'in recipe to determine to which repository or directory to '
5408 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005409 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005410 '-p', '--property', dest='properties', action='append', default=[],
5411 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005412 'key2=value2 etc. The value will be treated as '
5413 'json if decodable, or as string otherwise. '
5414 'NOTE: using this may make your try job not usable for CQ, '
5415 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005416 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005417 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5418 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005419 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005420 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005421 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005422 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005423 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005424 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005425
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005426 if options.master and options.master.startswith('luci.'):
5427 parser.error(
5428 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005429 # Make sure that all properties are prop=value pairs.
5430 bad_params = [x for x in options.properties if '=' not in x]
5431 if bad_params:
5432 parser.error('Got properties with missing "=": %s' % bad_params)
5433
maruel@chromium.org15192402012-09-06 12:38:29 +00005434 if args:
5435 parser.error('Unknown arguments: %s' % args)
5436
Koji Ishii31c14782018-01-08 17:17:33 +09005437 cl = Changelist(auth_config=auth_config, issue=options.issue,
5438 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005439 if not cl.GetIssue():
5440 parser.error('Need to upload first')
5441
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005442 if cl.IsGerrit():
5443 # HACK: warm up Gerrit change detail cache to save on RPCs.
5444 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5445
tandriie113dfd2016-10-11 10:20:12 -07005446 error_message = cl.CannotTriggerTryJobReason()
5447 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005448 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005449
borenet6c0efe62016-10-19 08:13:29 -07005450 if options.bucket and options.master:
5451 parser.error('Only one of --bucket and --master may be used.')
5452
qyearsley1fdfcb62016-10-24 13:22:03 -07005453 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005454
qyearsleydd49f942016-10-28 11:57:22 -07005455 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5456 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005457 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005458 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005459 print('git cl try with no bots now defaults to CQ dry run.')
5460 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5461 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005462
borenet6c0efe62016-10-19 08:13:29 -07005463 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005464 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005465 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005466 'of bot requires an initial job from a parent (usually a builder). '
5467 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005468 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005469 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005470
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005471 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005472 # TODO(tandrii): Checking local patchset against remote patchset is only
5473 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5474 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005475 print('Warning: Codereview server has newer patchsets (%s) than most '
5476 'recent upload from local checkout (%s). Did a previous upload '
5477 'fail?\n'
5478 'By default, git cl try uses the latest patchset from '
5479 'codereview, continuing to use patchset %s.\n' %
5480 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005481
tandrii568043b2016-10-11 07:49:18 -07005482 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005483 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005484 except BuildbucketResponseException as ex:
5485 print('ERROR: %s' % ex)
5486 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005487 return 0
5488
5489
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005490def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005491 """Prints info about try jobs associated with current CL."""
5492 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005493 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005494 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005495 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005496 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005497 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005498 '--color', action='store_true', default=setup_color.IS_TTY,
5499 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005500 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005501 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5502 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005503 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005504 '--json', help=('Path of JSON output file to write try job results to,'
5505 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005506 parser.add_option_group(group)
5507 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005508 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005509 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005510 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005511 if args:
5512 parser.error('Unrecognized args: %s' % ' '.join(args))
5513
5514 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005515 cl = Changelist(
5516 issue=options.issue, codereview=options.forced_codereview,
5517 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005518 if not cl.GetIssue():
5519 parser.error('Need to upload first')
5520
tandrii221ab252016-10-06 08:12:04 -07005521 patchset = options.patchset
5522 if not patchset:
5523 patchset = cl.GetMostRecentPatchset()
5524 if not patchset:
5525 parser.error('Codereview doesn\'t know about issue %s. '
5526 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005527 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005528 cl.GetIssue())
5529
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005530 # TODO(tandrii): Checking local patchset against remote patchset is only
5531 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5532 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005533 print('Warning: Codereview server has newer patchsets (%s) than most '
5534 'recent upload from local checkout (%s). Did a previous upload '
5535 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005536 'By default, git cl try-results uses the latest patchset from '
5537 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005538 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005539 try:
tandrii221ab252016-10-06 08:12:04 -07005540 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005541 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005542 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005543 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005544 if options.json:
5545 write_try_results_json(options.json, jobs)
5546 else:
5547 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005548 return 0
5549
5550
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005551@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005552def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005553 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005554 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005555 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005556 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005558 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005559 if args:
5560 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005561 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005562 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005563 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005564 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005565
5566 # Clear configured merge-base, if there is one.
5567 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005568 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005569 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005570 return 0
5571
5572
thestig@chromium.org00858c82013-12-02 23:08:03 +00005573def CMDweb(parser, args):
5574 """Opens the current CL in the web browser."""
5575 _, args = parser.parse_args(args)
5576 if args:
5577 parser.error('Unrecognized args: %s' % ' '.join(args))
5578
5579 issue_url = Changelist().GetIssueURL()
5580 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005581 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005582 return 1
5583
5584 webbrowser.open(issue_url)
5585 return 0
5586
5587
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005588def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005589 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005590 parser.add_option('-d', '--dry-run', action='store_true',
5591 help='trigger in dry run mode')
5592 parser.add_option('-c', '--clear', action='store_true',
5593 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005594 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005595 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005596 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005597 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005598 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005599 if args:
5600 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005601 if options.dry_run and options.clear:
5602 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5603
iannuccie53c9352016-08-17 14:40:40 -07005604 cl = Changelist(auth_config=auth_config, issue=options.issue,
5605 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005606 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005607 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005608 elif options.dry_run:
5609 state = _CQState.DRY_RUN
5610 else:
5611 state = _CQState.COMMIT
5612 if not cl.GetIssue():
5613 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005614 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005615 return 0
5616
5617
groby@chromium.org411034a2013-02-26 15:12:01 +00005618def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005619 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005620 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005621 auth.add_auth_options(parser)
5622 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005623 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005624 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005625 if args:
5626 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005627 cl = Changelist(auth_config=auth_config, issue=options.issue,
5628 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005629 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005630 if not cl.GetIssue():
5631 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005632 cl.CloseIssue()
5633 return 0
5634
5635
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005636def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005637 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005638 parser.add_option(
5639 '--stat',
5640 action='store_true',
5641 dest='stat',
5642 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005643 auth.add_auth_options(parser)
5644 options, args = parser.parse_args(args)
5645 auth_config = auth.extract_auth_config_from_options(options)
5646 if args:
5647 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005648
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005649 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005650 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005651 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005652 if not issue:
5653 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005654
Aaron Gablea718c3e2017-08-28 17:47:28 -07005655 base = cl._GitGetBranchConfigValue('last-upload-hash')
5656 if not base:
5657 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5658 if not base:
5659 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5660 revision_info = detail['revisions'][detail['current_revision']]
5661 fetch_info = revision_info['fetch']['http']
5662 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5663 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005664
Aaron Gablea718c3e2017-08-28 17:47:28 -07005665 cmd = ['git', 'diff']
5666 if options.stat:
5667 cmd.append('--stat')
5668 cmd.append(base)
5669 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005670
5671 return 0
5672
5673
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005674def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005675 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005676 parser.add_option(
5677 '--no-color',
5678 action='store_true',
5679 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005680 parser.add_option(
5681 '--batch',
5682 action='store_true',
5683 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005684 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005685 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005686 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005687
5688 author = RunGit(['config', 'user.email']).strip() or None
5689
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005690 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005691
5692 if args:
5693 if len(args) > 1:
5694 parser.error('Unknown args')
5695 base_branch = args[0]
5696 else:
5697 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005698 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005699
5700 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005701 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5702
5703 if options.batch:
5704 db = owners.Database(change.RepositoryRoot(), file, os.path)
5705 print('\n'.join(db.reviewers_for(affected_files, author)))
5706 return 0
5707
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005708 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005709 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005710 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005711 author,
5712 cl.GetReviewers(),
5713 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005714 disable_color=options.no_color,
5715 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005716
5717
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005718def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005719 """Generates a diff command."""
5720 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005721 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5722 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005723 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005724
5725 if args:
5726 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005727 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005728 diff_cmd.append(arg)
5729 else:
5730 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005731
5732 return diff_cmd
5733
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005734
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005735def MatchingFileType(file_name, extensions):
5736 """Returns true if the file name ends with one of the given extensions."""
5737 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005738
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005739
enne@chromium.org555cfe42014-01-29 18:21:39 +00005740@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005741def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005742 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005743 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005744 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005745 parser.add_option('--full', action='store_true',
5746 help='Reformat the full content of all touched files')
5747 parser.add_option('--dry-run', action='store_true',
5748 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005749 parser.add_option('--python', action='store_true',
5750 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005751 parser.add_option('--js', action='store_true',
5752 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005753 parser.add_option('--diff', action='store_true',
5754 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005755 parser.add_option('--presubmit', action='store_true',
5756 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005757 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005758
Daniel Chengc55eecf2016-12-30 03:11:02 -08005759 # Normalize any remaining args against the current path, so paths relative to
5760 # the current directory are still resolved as expected.
5761 args = [os.path.join(os.getcwd(), arg) for arg in args]
5762
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005763 # git diff generates paths against the root of the repository. Change
5764 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005765 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005766 if rel_base_path:
5767 os.chdir(rel_base_path)
5768
digit@chromium.org29e47272013-05-17 17:01:46 +00005769 # Grab the merge-base commit, i.e. the upstream commit of the current
5770 # branch when it was created or the last time it was rebased. This is
5771 # to cover the case where the user may have called "git fetch origin",
5772 # moving the origin branch to a newer commit, but hasn't rebased yet.
5773 upstream_commit = None
5774 cl = Changelist()
5775 upstream_branch = cl.GetUpstreamBranch()
5776 if upstream_branch:
5777 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5778 upstream_commit = upstream_commit.strip()
5779
5780 if not upstream_commit:
5781 DieWithError('Could not find base commit for this branch. '
5782 'Are you in detached state?')
5783
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005784 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5785 diff_output = RunGit(changed_files_cmd)
5786 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005787 # Filter out files deleted by this CL
5788 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005789
Christopher Lamc5ba6922017-01-24 11:19:14 +11005790 if opts.js:
5791 CLANG_EXTS.append('.js')
5792
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005793 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5794 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5795 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005796 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005797
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005798 top_dir = os.path.normpath(
5799 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5800
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005801 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5802 # formatted. This is used to block during the presubmit.
5803 return_value = 0
5804
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005805 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005806 # Locate the clang-format binary in the checkout
5807 try:
5808 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005809 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005810 DieWithError(e)
5811
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005812 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005813 cmd = [clang_format_tool]
5814 if not opts.dry_run and not opts.diff:
5815 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005816 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005817 if opts.diff:
5818 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005819 else:
5820 env = os.environ.copy()
5821 env['PATH'] = str(os.path.dirname(clang_format_tool))
5822 try:
5823 script = clang_format.FindClangFormatScriptInChromiumTree(
5824 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005825 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005826 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005827
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005828 cmd = [sys.executable, script, '-p0']
5829 if not opts.dry_run and not opts.diff:
5830 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005831
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005832 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5833 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005834
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005835 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5836 if opts.diff:
5837 sys.stdout.write(stdout)
5838 if opts.dry_run and len(stdout) > 0:
5839 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005840
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005841 # Similar code to above, but using yapf on .py files rather than clang-format
5842 # on C/C++ files
5843 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005844 yapf_tool = gclient_utils.FindExecutable('yapf')
5845 if yapf_tool is None:
5846 DieWithError('yapf not found in PATH')
5847
5848 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005849 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005850 cmd = [yapf_tool]
5851 if not opts.dry_run and not opts.diff:
5852 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005853 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005854 if opts.diff:
5855 sys.stdout.write(stdout)
5856 else:
5857 # TODO(sbc): yapf --lines mode still has some issues.
5858 # https://github.com/google/yapf/issues/154
5859 DieWithError('--python currently only works with --full')
5860
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005861 # Dart's formatter does not have the nice property of only operating on
5862 # modified chunks, so hard code full.
5863 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005864 try:
5865 command = [dart_format.FindDartFmtToolInChromiumTree()]
5866 if not opts.dry_run and not opts.diff:
5867 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005868 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005869
ppi@chromium.org6593d932016-03-03 15:41:15 +00005870 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005871 if opts.dry_run and stdout:
5872 return_value = 2
5873 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005874 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5875 'found in this checkout. Files in other languages are still '
5876 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005877
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005878 # Format GN build files. Always run on full build files for canonical form.
5879 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005880 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005881 if opts.dry_run or opts.diff:
5882 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005883 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005884 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5885 shell=sys.platform == 'win32',
5886 cwd=top_dir)
5887 if opts.dry_run and gn_ret == 2:
5888 return_value = 2 # Not formatted.
5889 elif opts.diff and gn_ret == 2:
5890 # TODO this should compute and print the actual diff.
5891 print("This change has GN build file diff for " + gn_diff_file)
5892 elif gn_ret != 0:
5893 # For non-dry run cases (and non-2 return values for dry-run), a
5894 # nonzero error code indicates a failure, probably because the file
5895 # doesn't parse.
5896 DieWithError("gn format failed on " + gn_diff_file +
5897 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005898
Ilya Shermane081cbe2017-08-15 17:51:04 -07005899 # Skip the metrics formatting from the global presubmit hook. These files have
5900 # a separate presubmit hook that issues an error if the files need formatting,
5901 # whereas the top-level presubmit script merely issues a warning. Formatting
5902 # these files is somewhat slow, so it's important not to duplicate the work.
5903 if not opts.presubmit:
5904 for xml_dir in GetDirtyMetricsDirs(diff_files):
5905 tool_dir = os.path.join(top_dir, xml_dir)
5906 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5907 if opts.dry_run or opts.diff:
5908 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005909 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005910 if opts.diff:
5911 sys.stdout.write(stdout)
5912 if opts.dry_run and stdout:
5913 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005914
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005915 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005916
Steven Holte2e664bf2017-04-21 13:10:47 -07005917def GetDirtyMetricsDirs(diff_files):
5918 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5919 metrics_xml_dirs = [
5920 os.path.join('tools', 'metrics', 'actions'),
5921 os.path.join('tools', 'metrics', 'histograms'),
5922 os.path.join('tools', 'metrics', 'rappor'),
5923 os.path.join('tools', 'metrics', 'ukm')]
5924 for xml_dir in metrics_xml_dirs:
5925 if any(file.startswith(xml_dir) for file in xml_diff_files):
5926 yield xml_dir
5927
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005928
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005929@subcommand.usage('<codereview url or issue id>')
5930def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005931 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005932 _, args = parser.parse_args(args)
5933
5934 if len(args) != 1:
5935 parser.print_help()
5936 return 1
5937
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005938 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005939 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005940 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005941
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005942 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005943
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005944 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005945 output = RunGit(['config', '--local', '--get-regexp',
5946 r'branch\..*\.%s' % issueprefix],
5947 error_ok=True)
5948 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005949 if issue == target_issue:
5950 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005951
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005952 branches = []
5953 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005954 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005955 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005956 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005957 return 1
5958 if len(branches) == 1:
5959 RunGit(['checkout', branches[0]])
5960 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005961 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005962 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005963 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005964 which = raw_input('Choose by index: ')
5965 try:
5966 RunGit(['checkout', branches[int(which)]])
5967 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005968 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005969 return 1
5970
5971 return 0
5972
5973
maruel@chromium.org29404b52014-09-08 22:58:00 +00005974def CMDlol(parser, args):
5975 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005976 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005977 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5978 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5979 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005980 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005981 return 0
5982
5983
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005984class OptionParser(optparse.OptionParser):
5985 """Creates the option parse and add --verbose support."""
5986 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005987 optparse.OptionParser.__init__(
5988 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005989 self.add_option(
5990 '-v', '--verbose', action='count', default=0,
5991 help='Use 2 times for more debugging info')
5992
5993 def parse_args(self, args=None, values=None):
5994 options, args = optparse.OptionParser.parse_args(self, args, values)
5995 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005996 logging.basicConfig(
5997 level=levels[min(options.verbose, len(levels) - 1)],
5998 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5999 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006000 return options, args
6001
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006003def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006004 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006005 print('\nYour python version %s is unsupported, please upgrade.\n' %
6006 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006007 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006008
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006009 # Reload settings.
6010 global settings
6011 settings = Settings()
6012
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006013 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006014 dispatcher = subcommand.CommandDispatcher(__name__)
6015 try:
6016 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006017 except auth.AuthenticationError as e:
6018 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006019 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006020 if e.code != 500:
6021 raise
6022 DieWithError(
6023 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6024 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006025 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006026
6027
6028if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006029 # These affect sys.stdout so do it outside of main() to simplify mocks in
6030 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006031 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006032 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006033 try:
6034 sys.exit(main(sys.argv[1:]))
6035 except KeyboardInterrupt:
6036 sys.stderr.write('interrupted\n')
6037 sys.exit(1)