blob: 5fa09d63ec813a65cd83e049642934dfdf94a349 [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:
433 auth_config: AuthConfig for Rietveld.
434 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
445 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000446 http = authenticator.authorize(httplib2.Http())
447 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700448
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000449 buildbucket_put_url = (
450 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000451 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700452 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
453 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
454 hostname=codereview_host,
455 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700457
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700458 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800459 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700460 if options.clobber:
461 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700462 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700463 if extra_properties:
464 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465
466 batch_req_body = {'builds': []}
467 print_text = []
468 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700469 for bucket, builders_and_tests in sorted(buckets.iteritems()):
470 print_text.append('Bucket: %s' % bucket)
471 master = None
472 if bucket.startswith(MASTER_PREFIX):
473 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474 for builder, tests in sorted(builders_and_tests.iteritems()):
475 print_text.append(' %s: %s' % (builder, tests))
476 parameters = {
477 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000478 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100479 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000480 'revision': options.revision,
481 }],
tandrii8c5a3532016-11-04 07:52:02 -0700482 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000484 if 'presubmit' in builder.lower():
485 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000486 if tests:
487 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700488
489 tags = [
490 'builder:%s' % builder,
491 'buildset:%s' % buildset,
492 'user_agent:git_cl_try',
493 ]
494 if master:
495 parameters['properties']['master'] = master
496 tags.append('master:%s' % master)
497
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 batch_req_body['builds'].append(
499 {
500 'bucket': bucket,
501 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000502 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700503 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000504 }
505 )
506
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700508 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 http,
510 buildbucket_put_url,
511 'PUT',
512 body=json.dumps(batch_req_body),
513 headers={'Content-Type': 'application/json'}
514 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000515 print_text.append('To see results here, run: git cl try-results')
516 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700517 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000518
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000519
tandrii221ab252016-10-06 08:12:04 -0700520def fetch_try_jobs(auth_config, changelist, buildbucket_host,
521 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700522 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000523
qyearsley53f48a12016-09-01 10:45:13 -0700524 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 """
tandrii221ab252016-10-06 08:12:04 -0700526 assert buildbucket_host
527 assert changelist.GetIssue(), 'CL must be uploaded first'
528 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
529 patchset = patchset or changelist.GetMostRecentPatchset()
530 assert patchset, 'CL must be uploaded first'
531
532 codereview_url = changelist.GetCodereviewServer()
533 codereview_host = urlparse.urlparse(codereview_url).hostname
534 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 if authenticator.has_cached_credentials():
536 http = authenticator.authorize(httplib2.Http())
537 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700538 print('Warning: Some results might be missing because %s' %
539 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700540 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 http = httplib2.Http()
542
543 http.force_exception_to_status_code = True
544
tandrii221ab252016-10-06 08:12:04 -0700545 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
546 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
547 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700549 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 params = {'tag': 'buildset:%s' % buildset}
551
552 builds = {}
553 while True:
554 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700557 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 for build in content.get('builds', []):
559 builds[build['id']] = build
560 if 'next_cursor' in content:
561 params['start_cursor'] = content['next_cursor']
562 else:
563 break
564 return builds
565
566
qyearsleyeab3c042016-08-24 09:18:28 -0700567def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 """Prints nicely result of fetch_try_jobs."""
569 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700570 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 return
572
573 # Make a copy, because we'll be modifying builds dictionary.
574 builds = builds.copy()
575 builder_names_cache = {}
576
577 def get_builder(b):
578 try:
579 return builder_names_cache[b['id']]
580 except KeyError:
581 try:
582 parameters = json.loads(b['parameters_json'])
583 name = parameters['builder_name']
584 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700585 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700586 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587 name = None
588 builder_names_cache[b['id']] = name
589 return name
590
591 def get_bucket(b):
592 bucket = b['bucket']
593 if bucket.startswith('master.'):
594 return bucket[len('master.'):]
595 return bucket
596
597 if options.print_master:
598 name_fmt = '%%-%ds %%-%ds' % (
599 max(len(str(get_bucket(b))) for b in builds.itervalues()),
600 max(len(str(get_builder(b))) for b in builds.itervalues()))
601 def get_name(b):
602 return name_fmt % (get_bucket(b), get_builder(b))
603 else:
604 name_fmt = '%%-%ds' % (
605 max(len(str(get_builder(b))) for b in builds.itervalues()))
606 def get_name(b):
607 return name_fmt % get_builder(b)
608
609 def sort_key(b):
610 return b['status'], b.get('result'), get_name(b), b.get('url')
611
612 def pop(title, f, color=None, **kwargs):
613 """Pop matching builds from `builds` dict and print them."""
614
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000615 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000616 colorize = str
617 else:
618 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
619
620 result = []
621 for b in builds.values():
622 if all(b.get(k) == v for k, v in kwargs.iteritems()):
623 builds.pop(b['id'])
624 result.append(b)
625 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700626 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000627 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700628 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629
630 total = len(builds)
631 pop(status='COMPLETED', result='SUCCESS',
632 title='Successes:', color=Fore.GREEN,
633 f=lambda b: (get_name(b), b.get('url')))
634 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
635 title='Infra Failures:', color=Fore.MAGENTA,
636 f=lambda b: (get_name(b), b.get('url')))
637 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
638 title='Failures:', color=Fore.RED,
639 f=lambda b: (get_name(b), b.get('url')))
640 pop(status='COMPLETED', result='CANCELED',
641 title='Canceled:', color=Fore.MAGENTA,
642 f=lambda b: (get_name(b),))
643 pop(status='COMPLETED', result='FAILURE',
644 failure_reason='INVALID_BUILD_DEFINITION',
645 title='Wrong master/builder name:', color=Fore.MAGENTA,
646 f=lambda b: (get_name(b),))
647 pop(status='COMPLETED', result='FAILURE',
648 title='Other failures:',
649 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
650 pop(status='COMPLETED',
651 title='Other finished:',
652 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
653 pop(status='STARTED',
654 title='Started:', color=Fore.YELLOW,
655 f=lambda b: (get_name(b), b.get('url')))
656 pop(status='SCHEDULED',
657 title='Scheduled:',
658 f=lambda b: (get_name(b), 'id=%s' % b['id']))
659 # The last section is just in case buildbucket API changes OR there is a bug.
660 pop(title='Other:',
661 f=lambda b: (get_name(b), 'id=%s' % b['id']))
662 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700663 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000664
665
qyearsley53f48a12016-09-01 10:45:13 -0700666def write_try_results_json(output_file, builds):
667 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
668
669 The input |builds| dict is assumed to be generated by Buildbucket.
670 Buildbucket documentation: http://goo.gl/G0s101
671 """
672
673 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800674 """Extracts some of the information from one build dict."""
675 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700676 return {
677 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700678 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800679 'builder_name': parameters.get('builder_name'),
680 'created_ts': build.get('created_ts'),
681 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700682 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800683 'result': build.get('result'),
684 'status': build.get('status'),
685 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700686 'url': build.get('url'),
687 }
688
689 converted = []
690 for _, build in sorted(builds.items()):
691 converted.append(convert_build_dict(build))
692 write_json(output_file, converted)
693
694
Aaron Gable13101a62018-02-09 13:20:41 -0800695def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000696 """Prints statistics about the change to the user."""
697 # --no-ext-diff is broken in some versions of Git, so try to work around
698 # this by overriding the environment (but there is still a problem if the
699 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000700 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000701 if 'GIT_EXTERNAL_DIFF' in env:
702 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000703
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000704 try:
705 stdout = sys.stdout.fileno()
706 except AttributeError:
707 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000708 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800709 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000710 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000711
712
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000713class BuildbucketResponseException(Exception):
714 pass
715
716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717class Settings(object):
718 def __init__(self):
719 self.default_server = None
720 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000721 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 self.tree_status_url = None
723 self.viewvc_url = None
724 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000725 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000726 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000727 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000728 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000729 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000730 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731
732 def LazyUpdateIfNeeded(self):
733 """Updates the settings from a codereview.settings file, if available."""
734 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000735 # The only value that actually changes the behavior is
736 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000737 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000738 error_ok=True
739 ).strip().lower()
740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000742 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 LoadCodereviewSettingsFromFile(cr_settings_file)
744 self.updated = True
745
746 def GetDefaultServerUrl(self, error_ok=False):
747 if not self.default_server:
748 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000749 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 if error_ok:
752 return self.default_server
753 if not self.default_server:
754 error_message = ('Could not find settings file. You must configure '
755 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000756 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 return self.default_server
759
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000760 @staticmethod
761 def GetRelativeRoot():
762 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000765 if self.root is None:
766 self.root = os.path.abspath(self.GetRelativeRoot())
767 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000769 def GetGitMirror(self, remote='origin'):
770 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000771 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000772 if not os.path.isdir(local_url):
773 return None
774 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
775 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100776 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100777 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000778 if mirror.exists():
779 return mirror
780 return None
781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 def GetTreeStatusUrl(self, error_ok=False):
783 if not self.tree_status_url:
784 error_message = ('You must configure your tree status URL by running '
785 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 self.tree_status_url = self._GetRietveldConfig(
787 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 return self.tree_status_url
789
790 def GetViewVCUrl(self):
791 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 return self.viewvc_url
794
rmistry@google.com90752582014-01-14 21:04:50 +0000795 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000797
rmistry@google.com78948ed2015-07-08 23:09:57 +0000798 def GetIsSkipDependencyUpload(self, branch_name):
799 """Returns true if specified branch should skip dep uploads."""
800 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
801 error_ok=True)
802
rmistry@google.com5626a922015-02-26 14:03:30 +0000803 def GetRunPostUploadHook(self):
804 run_post_upload_hook = self._GetRietveldConfig(
805 'run-post-upload-hook', error_ok=True)
806 return run_post_upload_hook == "True"
807
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000808 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000810
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000811 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700815 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700817 self.is_gerrit = (
818 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000819 return self.is_gerrit
820
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000821 def GetSquashGerritUploads(self):
822 """Return true if uploads to Gerrit should be squashed by default."""
823 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700824 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
825 if self.squash_gerrit_uploads is None:
826 # Default is squash now (http://crbug.com/611892#c23).
827 self.squash_gerrit_uploads = not (
828 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
829 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000830 return self.squash_gerrit_uploads
831
tandriia60502f2016-06-20 02:01:53 -0700832 def GetSquashGerritUploadsOverride(self):
833 """Return True or False if codereview.settings should be overridden.
834
835 Returns None if no override has been defined.
836 """
837 # See also http://crbug.com/611892#c23
838 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
839 error_ok=True).strip()
840 if result == 'true':
841 return True
842 if result == 'false':
843 return False
844 return None
845
tandrii@chromium.org28253532016-04-14 13:46:56 +0000846 def GetGerritSkipEnsureAuthenticated(self):
847 """Return True if EnsureAuthenticated should not be done for Gerrit
848 uploads."""
849 if self.gerrit_skip_ensure_authenticated is None:
850 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000851 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000852 error_ok=True).strip() == 'true')
853 return self.gerrit_skip_ensure_authenticated
854
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000855 def GetGitEditor(self):
856 """Return the editor specified in the git config, or None if none is."""
857 if self.git_editor is None:
858 self.git_editor = self._GetConfig('core.editor', error_ok=True)
859 return self.git_editor or None
860
thestig@chromium.org44202a22014-03-11 19:22:18 +0000861 def GetLintRegex(self):
862 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
863 DEFAULT_LINT_REGEX)
864
865 def GetLintIgnoreRegex(self):
866 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
867 DEFAULT_LINT_IGNORE_REGEX)
868
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000869 def GetProject(self):
870 if not self.project:
871 self.project = self._GetRietveldConfig('project', error_ok=True)
872 return self.project
873
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000874 def _GetRietveldConfig(self, param, **kwargs):
875 return self._GetConfig('rietveld.' + param, **kwargs)
876
rmistry@google.com78948ed2015-07-08 23:09:57 +0000877 def _GetBranchConfig(self, branch_name, param, **kwargs):
878 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 def _GetConfig(self, param, **kwargs):
881 self.LazyUpdateIfNeeded()
882 return RunGit(['config', param], **kwargs).strip()
883
884
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100885@contextlib.contextmanager
886def _get_gerrit_project_config_file(remote_url):
887 """Context manager to fetch and store Gerrit's project.config from
888 refs/meta/config branch and store it in temp file.
889
890 Provides a temporary filename or None if there was error.
891 """
892 error, _ = RunGitWithCode([
893 'fetch', remote_url,
894 '+refs/meta/config:refs/git_cl/meta/config'])
895 if error:
896 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700897 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100898 (remote_url, error))
899 yield None
900 return
901
902 error, project_config_data = RunGitWithCode(
903 ['show', 'refs/git_cl/meta/config:project.config'])
904 if error:
905 print('WARNING: project.config file not found')
906 yield None
907 return
908
909 with gclient_utils.temporary_directory() as tempdir:
910 project_config_file = os.path.join(tempdir, 'project.config')
911 gclient_utils.FileWrite(project_config_file, project_config_data)
912 yield project_config_file
913
914
915def _is_git_numberer_enabled(remote_url, remote_ref):
916 """Returns True if Git Numberer is enabled on this ref."""
917 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100918 KNOWN_PROJECTS_WHITELIST = [
919 'chromium/src',
920 'external/webrtc',
921 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100922 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200923 # For webrtc.googlesource.com/src.
924 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100925 ]
926
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100927 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
928 url_parts = urlparse.urlparse(remote_url)
929 project_name = url_parts.path.lstrip('/').rstrip('git./')
930 for known in KNOWN_PROJECTS_WHITELIST:
931 if project_name.endswith(known):
932 break
933 else:
934 # Early exit to avoid extra fetches for repos that aren't using Git
935 # Numberer.
936 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100937
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100938 with _get_gerrit_project_config_file(remote_url) as project_config_file:
939 if project_config_file is None:
940 # Failed to fetch project.config, which shouldn't happen on open source
941 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100942 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100943 def get_opts(x):
944 code, out = RunGitWithCode(
945 ['config', '-f', project_config_file, '--get-all',
946 'plugin.git-numberer.validate-%s-refglob' % x])
947 if code == 0:
948 return out.strip().splitlines()
949 return []
950 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100951
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100952 logging.info('validator config enabled %s disabled %s refglobs for '
953 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000954
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100955 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100956 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100957 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100958 return True
959 return False
960
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100961 if match_refglobs(disabled):
962 return False
963 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100964
965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966def ShortBranchName(branch):
967 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000968 return branch.replace('refs/heads/', '', 1)
969
970
971def GetCurrentBranchRef():
972 """Returns branch ref (e.g., refs/heads/master) or None."""
973 return RunGit(['symbolic-ref', 'HEAD'],
974 stderr=subprocess2.VOID, error_ok=True).strip() or None
975
976
977def GetCurrentBranch():
978 """Returns current branch or None.
979
980 For refs/heads/* branches, returns just last part. For others, full ref.
981 """
982 branchref = GetCurrentBranchRef()
983 if branchref:
984 return ShortBranchName(branchref)
985 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986
987
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000988class _CQState(object):
989 """Enum for states of CL with respect to Commit Queue."""
990 NONE = 'none'
991 DRY_RUN = 'dry_run'
992 COMMIT = 'commit'
993
994 ALL_STATES = [NONE, DRY_RUN, COMMIT]
995
996
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000997class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200998 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999 self.issue = issue
1000 self.patchset = patchset
1001 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001002 assert codereview in (None, 'rietveld', 'gerrit')
1003 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001004
1005 @property
1006 def valid(self):
1007 return self.issue is not None
1008
1009
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001010def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001011 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1012 fail_result = _ParsedIssueNumberArgument()
1013
1014 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001015 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001016 if not arg.startswith('http'):
1017 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001018
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001019 url = gclient_utils.UpgradeToHttps(arg)
1020 try:
1021 parsed_url = urlparse.urlparse(url)
1022 except ValueError:
1023 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001024
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001025 if codereview is not None:
1026 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1027 return parsed or fail_result
1028
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001029 results = {}
1030 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1031 parsed = cls.ParseIssueURL(parsed_url)
1032 if parsed is not None:
1033 results[name] = parsed
1034
1035 if not results:
1036 return fail_result
1037 if len(results) == 1:
1038 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001039
1040 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1041 # This is likely Gerrit.
1042 return results['gerrit']
1043 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001044 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045
1046
Aaron Gablea45ee112016-11-22 15:14:38 -08001047class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001048 def __init__(self, issue, url):
1049 self.issue = issue
1050 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001051 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001052
1053 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001054 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001055 self.issue, self.url)
1056
1057
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001058_CommentSummary = collections.namedtuple(
1059 '_CommentSummary', ['date', 'message', 'sender',
1060 # TODO(tandrii): these two aren't known in Gerrit.
1061 'approval', 'disapproval'])
1062
1063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001065 """Changelist works with one changelist in local branch.
1066
1067 Supports two codereview backends: Rietveld or Gerrit, selected at object
1068 creation.
1069
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001070 Notes:
1071 * Not safe for concurrent multi-{thread,process} use.
1072 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001073 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001074 """
1075
1076 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1077 """Create a new ChangeList instance.
1078
1079 If issue is given, the codereview must be given too.
1080
1081 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1082 Otherwise, it's decided based on current configuration of the local branch,
1083 with default being 'rietveld' for backwards compatibility.
1084 See _load_codereview_impl for more details.
1085
1086 **kwargs will be passed directly to codereview implementation.
1087 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001089 global settings
1090 if not settings:
1091 # Happens when git_cl.py is used as a utility library.
1092 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093
1094 if issue:
1095 assert codereview, 'codereview must be known, if issue is known'
1096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.branchref = branchref
1098 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001099 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.branch = ShortBranchName(self.branchref)
1101 else:
1102 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001104 self.lookedup_issue = False
1105 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = False
1107 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001108 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001110 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001111 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001112 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001113
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001115 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 assert self._codereview_impl
1118 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119
1120 def _load_codereview_impl(self, codereview=None, **kwargs):
1121 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001122 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1123 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1124 self._codereview = codereview
1125 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001126 return
1127
1128 # Automatic selection based on issue number set for a current branch.
1129 # Rietveld takes precedence over Gerrit.
1130 assert not self.issue
1131 # Whether we find issue or not, we are doing the lookup.
1132 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001133 if self.GetBranch():
1134 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1135 issue = _git_get_branch_config_value(
1136 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1137 if issue:
1138 self._codereview = codereview
1139 self._codereview_impl = cls(self, **kwargs)
1140 self.issue = int(issue)
1141 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142
1143 # No issue is set for this branch, so decide based on repo-wide settings.
1144 return self._load_codereview_impl(
1145 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1146 **kwargs)
1147
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001148 def IsGerrit(self):
1149 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150
1151 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001152 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001154 The return value is a string suitable for passing to git cl with the --cc
1155 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001156 """
1157 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001158 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001159 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001160 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1161 return self.cc
1162
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001163 def GetCCListWithoutDefault(self):
1164 """Return the users cc'd on this CL excluding default ones."""
1165 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001166 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001167 return self.cc
1168
Daniel Cheng7227d212017-11-17 08:12:37 -08001169 def ExtendCC(self, more_cc):
1170 """Extends the list of users to cc on this CL based on the changed files."""
1171 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
1173 def GetBranch(self):
1174 """Returns the short branch name, e.g. 'master'."""
1175 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001177 if not branchref:
1178 return None
1179 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 self.branch = ShortBranchName(self.branchref)
1181 return self.branch
1182
1183 def GetBranchRef(self):
1184 """Returns the full branch name, e.g. 'refs/heads/master'."""
1185 self.GetBranch() # Poke the lazy loader.
1186 return self.branchref
1187
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001188 def ClearBranch(self):
1189 """Clears cached branch data of this object."""
1190 self.branch = self.branchref = None
1191
tandrii5d48c322016-08-18 16:19:37 -07001192 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1193 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1194 kwargs['branch'] = self.GetBranch()
1195 return _git_get_branch_config_value(key, default, **kwargs)
1196
1197 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1198 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1199 assert self.GetBranch(), (
1200 'this CL must have an associated branch to %sset %s%s' %
1201 ('un' if value is None else '',
1202 key,
1203 '' if value is None else ' to %r' % value))
1204 kwargs['branch'] = self.GetBranch()
1205 return _git_set_branch_config_value(key, value, **kwargs)
1206
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 @staticmethod
1208 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001209 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 e.g. 'origin', 'refs/heads/master'
1211 """
1212 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001213 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1214
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001216 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001218 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1219 error_ok=True).strip()
1220 if upstream_branch:
1221 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001223 # Else, try to guess the origin remote.
1224 remote_branches = RunGit(['branch', '-r']).split()
1225 if 'origin/master' in remote_branches:
1226 # Fall back on origin/master if it exits.
1227 remote = 'origin'
1228 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001230 DieWithError(
1231 'Unable to determine default branch to diff against.\n'
1232 'Either pass complete "git diff"-style arguments, like\n'
1233 ' git cl upload origin/master\n'
1234 'or verify this branch is set up to track another \n'
1235 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
1237 return remote, upstream_branch
1238
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001239 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001240 upstream_branch = self.GetUpstreamBranch()
1241 if not BranchExists(upstream_branch):
1242 DieWithError('The upstream for the current branch (%s) does not exist '
1243 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001244 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001245 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 def GetUpstreamBranch(self):
1248 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001251 upstream_branch = upstream_branch.replace('refs/heads/',
1252 'refs/remotes/%s/' % remote)
1253 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1254 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 self.upstream_branch = upstream_branch
1256 return self.upstream_branch
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001259 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001260 remote, branch = None, self.GetBranch()
1261 seen_branches = set()
1262 while branch not in seen_branches:
1263 seen_branches.add(branch)
1264 remote, branch = self.FetchUpstreamTuple(branch)
1265 branch = ShortBranchName(branch)
1266 if remote != '.' or branch.startswith('refs/remotes'):
1267 break
1268 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001269 remotes = RunGit(['remote'], error_ok=True).split()
1270 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001271 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001272 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 logging.warn('Could not determine which remote this change is '
1275 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001276 else:
1277 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001278 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 branch = 'HEAD'
1280 if branch.startswith('refs/remotes'):
1281 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001282 elif branch.startswith('refs/branch-heads/'):
1283 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 else:
1285 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001286 return self._remote
1287
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 def GitSanityChecks(self, upstream_git_obj):
1289 """Checks git repo status and ensures diff is from local commits."""
1290
sbc@chromium.org79706062015-01-14 21:18:12 +00001291 if upstream_git_obj is None:
1292 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001293 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001294 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001295 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001296 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001297 return False
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 # Verify the commit we're diffing against is in our current branch.
1300 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1301 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1302 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001303 print('ERROR: %s is not in the current branch. You may need to rebase '
1304 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 return False
1306
1307 # List the commits inside the diff, and verify they are all local.
1308 commits_in_diff = RunGit(
1309 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1310 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1311 remote_branch = remote_branch.strip()
1312 if code != 0:
1313 _, remote_branch = self.GetRemoteBranch()
1314
1315 commits_in_remote = RunGit(
1316 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1317
1318 common_commits = set(commits_in_diff) & set(commits_in_remote)
1319 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001320 print('ERROR: Your diff contains %d commits already in %s.\n'
1321 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1322 'the diff. If you are using a custom git flow, you can override'
1323 ' the reference used for this check with "git config '
1324 'gitcl.remotebranch <git-ref>".' % (
1325 len(common_commits), remote_branch, upstream_git_obj),
1326 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 return False
1328 return True
1329
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001330 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001331 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001332
1333 Returns None if it is not set.
1334 """
tandrii5d48c322016-08-18 16:19:37 -07001335 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 def GetRemoteUrl(self):
1338 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1339
1340 Returns None if there is no remote.
1341 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001342 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001343 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1344
1345 # If URL is pointing to a local directory, it is probably a git cache.
1346 if os.path.isdir(url):
1347 url = RunGit(['config', 'remote.%s.url' % remote],
1348 error_ok=True,
1349 cwd=url).strip()
1350 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001352 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001353 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001354 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001355 self.issue = self._GitGetBranchConfigValue(
1356 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001357 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 return self.issue
1359
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 def GetIssueURL(self):
1361 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001362 issue = self.GetIssue()
1363 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001364 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001365 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001367 def GetDescription(self, pretty=False, force=False):
1368 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001370 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 self.has_description = True
1372 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001373 # Set width to 72 columns + 2 space indent.
1374 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001376 lines = self.description.splitlines()
1377 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 return self.description
1379
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001380 def GetDescriptionFooters(self):
1381 """Returns (non_footer_lines, footers) for the commit message.
1382
1383 Returns:
1384 non_footer_lines (list(str)) - Simple list of description lines without
1385 any footer. The lines do not contain newlines, nor does the list contain
1386 the empty line between the message and the footers.
1387 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1388 [("Change-Id", "Ideadbeef...."), ...]
1389 """
1390 raw_description = self.GetDescription()
1391 msg_lines, _, footers = git_footers.split_footers(raw_description)
1392 if footers:
1393 msg_lines = msg_lines[:len(msg_lines)-1]
1394 return msg_lines, footers
1395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001397 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001399 self.patchset = self._GitGetBranchConfigValue(
1400 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 return self.patchset
1403
1404 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001405 """Set this branch's patchset. If patchset=0, clears the patchset."""
1406 assert self.GetBranch()
1407 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001408 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001409 else:
1410 self.patchset = int(patchset)
1411 self._GitSetBranchConfigValue(
1412 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001414 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001415 """Set this branch's issue. If issue isn't given, clears the issue."""
1416 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001418 issue = int(issue)
1419 self._GitSetBranchConfigValue(
1420 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001421 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001422 codereview_server = self._codereview_impl.GetCodereviewServer()
1423 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001424 self._GitSetBranchConfigValue(
1425 self._codereview_impl.CodereviewServerConfigKey(),
1426 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 else:
tandrii5d48c322016-08-18 16:19:37 -07001428 # Reset all of these just to be clean.
1429 reset_suffixes = [
1430 'last-upload-hash',
1431 self._codereview_impl.IssueConfigKey(),
1432 self._codereview_impl.PatchsetConfigKey(),
1433 self._codereview_impl.CodereviewServerConfigKey(),
1434 ] + self._PostUnsetIssueProperties()
1435 for prop in reset_suffixes:
1436 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001437 msg = RunGit(['log', '-1', '--format=%B']).strip()
1438 if msg and git_footers.get_footer_change_id(msg):
1439 print('WARNING: The change patched into this branch has a Change-Id. '
1440 'Removing it.')
1441 RunGit(['commit', '--amend', '-m',
1442 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001444 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445
dnjba1b0f32016-09-02 12:37:42 -07001446 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001447 if not self.GitSanityChecks(upstream_branch):
1448 DieWithError('\nGit sanity check failure')
1449
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001450 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001451 if not root:
1452 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001453 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001454
1455 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001456 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001457 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001458 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001459 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 except subprocess2.CalledProcessError:
1461 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001462 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001463 'This branch probably doesn\'t exist anymore. To reset the\n'
1464 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001465 ' git branch --set-upstream-to origin/master %s\n'
1466 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001468
maruel@chromium.org52424302012-08-29 15:14:30 +00001469 issue = self.GetIssue()
1470 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001471 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001472 description = self.GetDescription()
1473 else:
1474 # If the change was never uploaded, use the log messages of all commits
1475 # up to the branch point, as git cl upload will prefill the description
1476 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001477 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1478 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001479
1480 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001481 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001482 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001483 name,
1484 description,
1485 absroot,
1486 files,
1487 issue,
1488 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001489 author,
1490 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001491
dsansomee2d6fd92016-09-08 00:10:47 -07001492 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001493 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001494 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001497 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1498 """Sets the description for this CL remotely.
1499
1500 You can get description_lines and footers with GetDescriptionFooters.
1501
1502 Args:
1503 description_lines (list(str)) - List of CL description lines without
1504 newline characters.
1505 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1506 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1507 `List-Of-Tokens`). It will be case-normalized so that each token is
1508 title-cased.
1509 """
1510 new_description = '\n'.join(description_lines)
1511 if footers:
1512 new_description += '\n'
1513 for k, v in footers:
1514 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1515 if not git_footers.FOOTER_PATTERN.match(foot):
1516 raise ValueError('Invalid footer %r' % foot)
1517 new_description += foot + '\n'
1518 self.UpdateDescription(new_description, force)
1519
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001520 def RunHook(self, committing, may_prompt, verbose, change):
1521 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1522 try:
1523 return presubmit_support.DoPresubmitChecks(change, committing,
1524 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1525 default_presubmit=None, may_prompt=may_prompt,
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001526 rietveld_obj=self._codereview_impl.GetRietveldObjForPresubmit(),
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001527 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001528 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001529 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001531 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1532 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001533 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1534 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001535 else:
1536 # Assume url.
1537 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1538 urlparse.urlparse(issue_arg))
1539 if not parsed_issue_arg or not parsed_issue_arg.valid:
1540 DieWithError('Failed to parse issue argument "%s". '
1541 'Must be an issue number or a valid URL.' % issue_arg)
1542 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001543 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001544
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 def CMDUpload(self, options, git_diff_args, orig_args):
1546 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001547 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001549 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550 else:
1551 if self.GetBranch() is None:
1552 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1553
1554 # Default to diffing against common ancestor of upstream branch
1555 base_branch = self.GetCommonAncestorWithUpstream()
1556 git_diff_args = [base_branch, 'HEAD']
1557
Aaron Gablec4c40d12017-05-22 11:49:53 -07001558 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1559 if not self.IsGerrit() and not self.GetIssue():
1560 print('=====================================')
1561 print('NOTICE: Rietveld is being deprecated. '
1562 'You can upload changes to Gerrit with')
1563 print(' git cl upload --gerrit')
1564 print('or set Gerrit to be your default code review tool with')
1565 print(' git config gerrit.host true')
1566 print('=====================================')
1567
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001568 # Fast best-effort checks to abort before running potentially
1569 # expensive hooks if uploading is likely to fail anyway. Passing these
1570 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001571 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001572 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573
1574 # Apply watchlists on upload.
1575 change = self.GetChange(base_branch, None)
1576 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1577 files = [f.LocalPath() for f in change.AffectedFiles()]
1578 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001579 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580
1581 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001582 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583 # Set the reviewer list now so that presubmit checks can access it.
1584 change_description = ChangeDescription(change.FullDescriptionText())
1585 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001586 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001587 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 change)
1589 change.SetDescriptionText(change_description.description)
1590 hook_results = self.RunHook(committing=False,
1591 may_prompt=not options.force,
1592 verbose=options.verbose,
1593 change=change)
1594 if not hook_results.should_continue():
1595 return 1
1596 if not options.reviewers and hook_results.reviewers:
1597 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001598 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001600 # TODO(tandrii): Checking local patchset against remote patchset is only
1601 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1602 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603 latest_patchset = self.GetMostRecentPatchset()
1604 local_patchset = self.GetPatchset()
1605 if (latest_patchset and local_patchset and
1606 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001607 print('The last upload made from this repository was patchset #%d but '
1608 'the most recent patchset on the server is #%d.'
1609 % (local_patchset, latest_patchset))
1610 print('Uploading will still work, but if you\'ve uploaded to this '
1611 'issue from another machine or branch the patch you\'re '
1612 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001613 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614
Aaron Gable13101a62018-02-09 13:20:41 -08001615 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001616 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001618 if self.IsGerrit():
1619 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1620 options.cq_dry_run);
1621 else:
1622 if options.use_commit_queue:
1623 self.SetCQState(_CQState.COMMIT)
1624 elif options.cq_dry_run:
1625 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001626
tandrii5d48c322016-08-18 16:19:37 -07001627 _git_set_branch_config_value('last-upload-hash',
1628 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629 # Run post upload hooks, if specified.
1630 if settings.GetRunPostUploadHook():
1631 presubmit_support.DoPostUploadExecuter(
1632 change,
1633 self,
1634 settings.GetRoot(),
1635 options.verbose,
1636 sys.stdout)
1637
1638 # Upload all dependencies if specified.
1639 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001640 print()
1641 print('--dependencies has been specified.')
1642 print('All dependent local branches will be re-uploaded.')
1643 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644 # Remove the dependencies flag from args so that we do not end up in a
1645 # loop.
1646 orig_args.remove('--dependencies')
1647 ret = upload_branch_deps(self, orig_args)
1648 return ret
1649
Ravi Mistry31e7d562018-04-02 12:53:57 -04001650 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1651 """Sets labels on the change based on the provided flags.
1652
1653 Sets labels if issue is already uploaded and known, else returns without
1654 doing anything.
1655
1656 Args:
1657 enable_auto_submit: Sets Auto-Submit+1 on the change.
1658 use_commit_queue: Sets Commit-Queue+2 on the change.
1659 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1660 both use_commit_queue and cq_dry_run are true.
1661 """
1662 if not self.GetIssue():
1663 return
1664 try:
1665 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1666 cq_dry_run)
1667 return 0
1668 except KeyboardInterrupt:
1669 raise
1670 except:
1671 labels = []
1672 if enable_auto_submit:
1673 labels.append('Auto-Submit')
1674 if use_commit_queue or cq_dry_run:
1675 labels.append('Commit-Queue')
1676 print('WARNING: Failed to set label(s) on your change: %s\n'
1677 'Either:\n'
1678 ' * Your project does not have the above label(s),\n'
1679 ' * You don\'t have permission to set the above label(s),\n'
1680 ' * There\'s a bug in this code (see stack trace below).\n' %
1681 (', '.join(labels)))
1682 # Still raise exception so that stack trace is printed.
1683 raise
1684
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001685 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001686 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001687
1688 Issue must have been already uploaded and known.
1689 """
1690 assert new_state in _CQState.ALL_STATES
1691 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001692 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001693 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001694 return 0
1695 except KeyboardInterrupt:
1696 raise
1697 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001698 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001699 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001700 ' * Your project has no CQ,\n'
1701 ' * You don\'t have permission to change the CQ state,\n'
1702 ' * There\'s a bug in this code (see stack trace below).\n'
1703 'Consider specifying which bots to trigger manually or asking your '
1704 'project owners for permissions or contacting Chrome Infra at:\n'
1705 'https://www.chromium.org/infra\n\n' %
1706 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001707 # Still raise exception so that stack trace is printed.
1708 raise
1709
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001710 # Forward methods to codereview specific implementation.
1711
Aaron Gable636b13f2017-07-14 10:42:48 -07001712 def AddComment(self, message, publish=None):
1713 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001714
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001715 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001716 """Returns list of _CommentSummary for each comment.
1717
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001718 args:
1719 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001720 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001721 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001722
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 def CloseIssue(self):
1724 return self._codereview_impl.CloseIssue()
1725
1726 def GetStatus(self):
1727 return self._codereview_impl.GetStatus()
1728
1729 def GetCodereviewServer(self):
1730 return self._codereview_impl.GetCodereviewServer()
1731
tandriide281ae2016-10-12 06:02:30 -07001732 def GetIssueOwner(self):
1733 """Get owner from codereview, which may differ from this checkout."""
1734 return self._codereview_impl.GetIssueOwner()
1735
Edward Lemur707d70b2018-02-07 00:50:14 +01001736 def GetReviewers(self):
1737 return self._codereview_impl.GetReviewers()
1738
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001739 def GetMostRecentPatchset(self):
1740 return self._codereview_impl.GetMostRecentPatchset()
1741
tandriide281ae2016-10-12 06:02:30 -07001742 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001743 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001744 return self._codereview_impl.CannotTriggerTryJobReason()
1745
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001746 def GetTryJobProperties(self, patchset=None):
1747 """Returns dictionary of properties to launch try job."""
1748 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001749
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 def __getattr__(self, attr):
1751 # This is because lots of untested code accesses Rietveld-specific stuff
1752 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001753 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001754 # Note that child method defines __getattr__ as well, and forwards it here,
1755 # because _RietveldChangelistImpl is not cleaned up yet, and given
1756 # deprecation of Rietveld, it should probably be just removed.
1757 # Until that time, avoid infinite recursion by bypassing __getattr__
1758 # of implementation class.
1759 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001760
1761
1762class _ChangelistCodereviewBase(object):
1763 """Abstract base class encapsulating codereview specifics of a changelist."""
1764 def __init__(self, changelist):
1765 self._changelist = changelist # instance of Changelist
1766
1767 def __getattr__(self, attr):
1768 # Forward methods to changelist.
1769 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1770 # _RietveldChangelistImpl to avoid this hack?
1771 return getattr(self._changelist, attr)
1772
1773 def GetStatus(self):
1774 """Apply a rough heuristic to give a simple summary of an issue's review
1775 or CQ status, assuming adherence to a common workflow.
1776
1777 Returns None if no issue for this branch, or specific string keywords.
1778 """
1779 raise NotImplementedError()
1780
1781 def GetCodereviewServer(self):
1782 """Returns server URL without end slash, like "https://codereview.com"."""
1783 raise NotImplementedError()
1784
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001785 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786 """Fetches and returns description from the codereview server."""
1787 raise NotImplementedError()
1788
tandrii5d48c322016-08-18 16:19:37 -07001789 @classmethod
1790 def IssueConfigKey(cls):
1791 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001792 raise NotImplementedError()
1793
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001794 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001795 def PatchsetConfigKey(cls):
1796 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 raise NotImplementedError()
1798
tandrii5d48c322016-08-18 16:19:37 -07001799 @classmethod
1800 def CodereviewServerConfigKey(cls):
1801 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 raise NotImplementedError()
1803
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001804 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001805 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001806 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001807
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001808 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001809 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001810 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 raise NotImplementedError()
1812
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001813 def GetGerritObjForPresubmit(self):
1814 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1815 return None
1816
dsansomee2d6fd92016-09-08 00:10:47 -07001817 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 """Update the description on codereview site."""
1819 raise NotImplementedError()
1820
Aaron Gable636b13f2017-07-14 10:42:48 -07001821 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001822 """Posts a comment to the codereview site."""
1823 raise NotImplementedError()
1824
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001825 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def CloseIssue(self):
1829 """Closes the issue."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 def GetMostRecentPatchset(self):
1833 """Returns the most recent patchset number from the codereview site."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001836 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001837 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001838 """Fetches and applies the issue.
1839
1840 Arguments:
1841 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1842 reject: if True, reject the failed patch instead of switching to 3-way
1843 merge. Rietveld only.
1844 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1845 only.
1846 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001847 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001848 """
1849 raise NotImplementedError()
1850
1851 @staticmethod
1852 def ParseIssueURL(parsed_url):
1853 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1854 failed."""
1855 raise NotImplementedError()
1856
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001858 """Best effort check that user is authenticated with codereview server.
1859
1860 Arguments:
1861 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001862 refresh: whether to attempt to refresh credentials. Ignored if not
1863 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001865 raise NotImplementedError()
1866
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001867 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001868 """Best effort check that uploading isn't supposed to fail for predictable
1869 reasons.
1870
1871 This method should raise informative exception if uploading shouldn't
1872 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001873
1874 Arguments:
1875 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001877 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001878
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001879 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001880 """Uploads a change to codereview."""
1881 raise NotImplementedError()
1882
Ravi Mistry31e7d562018-04-02 12:53:57 -04001883 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1884 """Sets labels on the change based on the provided flags.
1885
1886 Issue must have been already uploaded and known.
1887 """
1888 raise NotImplementedError()
1889
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001890 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001891 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001892
1893 Issue must have been already uploaded and known.
1894 """
1895 raise NotImplementedError()
1896
tandriie113dfd2016-10-11 10:20:12 -07001897 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001898 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001899 raise NotImplementedError()
1900
tandriide281ae2016-10-12 06:02:30 -07001901 def GetIssueOwner(self):
1902 raise NotImplementedError()
1903
Edward Lemur707d70b2018-02-07 00:50:14 +01001904 def GetReviewers(self):
1905 raise NotImplementedError()
1906
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001907 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001908 raise NotImplementedError()
1909
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001910
1911class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001912
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001913 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914 super(_RietveldChangelistImpl, self).__init__(changelist)
1915 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001916 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001917 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001918
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001919 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001920 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001921 self._props = None
1922 self._rpc_server = None
1923
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 def GetCodereviewServer(self):
1925 if not self._rietveld_server:
1926 # If we're on a branch then get the server potentially associated
1927 # with that branch.
1928 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001929 self._rietveld_server = gclient_utils.UpgradeToHttps(
1930 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931 if not self._rietveld_server:
1932 self._rietveld_server = settings.GetDefaultServerUrl()
1933 return self._rietveld_server
1934
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001935 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001936 """Best effort check that user is authenticated with Rietveld server."""
1937 if self._auth_config.use_oauth2:
1938 authenticator = auth.get_authenticator_for_host(
1939 self.GetCodereviewServer(), self._auth_config)
1940 if not authenticator.has_cached_credentials():
1941 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001942 if refresh:
1943 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001944
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001945 def EnsureCanUploadPatchset(self, force):
1946 # No checks for Rietveld because we are deprecating Rietveld.
1947 pass
1948
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001949 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001950 issue = self.GetIssue()
1951 assert issue
1952 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001953 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 except urllib2.HTTPError as e:
1955 if e.code == 404:
1956 DieWithError(
1957 ('\nWhile fetching the description for issue %d, received a '
1958 '404 (not found)\n'
1959 'error. It is likely that you deleted this '
1960 'issue on the server. If this is the\n'
1961 'case, please run\n\n'
1962 ' git cl issue 0\n\n'
1963 'to clear the association with the deleted issue. Then run '
1964 'this command again.') % issue)
1965 else:
1966 DieWithError(
1967 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1968 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001969 print('Warning: Failed to retrieve CL description due to network '
1970 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001971 return ''
1972
1973 def GetMostRecentPatchset(self):
1974 return self.GetIssueProperties()['patchsets'][-1]
1975
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001976 def GetIssueProperties(self):
1977 if self._props is None:
1978 issue = self.GetIssue()
1979 if not issue:
1980 self._props = {}
1981 else:
1982 self._props = self.RpcServer().get_issue_properties(issue, True)
1983 return self._props
1984
tandriie113dfd2016-10-11 10:20:12 -07001985 def CannotTriggerTryJobReason(self):
1986 props = self.GetIssueProperties()
1987 if not props:
1988 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1989 if props.get('closed'):
1990 return 'CL %s is closed' % self.GetIssue()
1991 if props.get('private'):
1992 return 'CL %s is private' % self.GetIssue()
1993 return None
1994
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001995 def GetTryJobProperties(self, patchset=None):
1996 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001997 project = (self.GetIssueProperties() or {}).get('project')
1998 return {
1999 'issue': self.GetIssue(),
2000 'patch_project': project,
2001 'patch_storage': 'rietveld',
2002 'patchset': patchset or self.GetPatchset(),
2003 'rietveld': self.GetCodereviewServer(),
2004 }
2005
tandriide281ae2016-10-12 06:02:30 -07002006 def GetIssueOwner(self):
2007 return (self.GetIssueProperties() or {}).get('owner_email')
2008
Edward Lemur707d70b2018-02-07 00:50:14 +01002009 def GetReviewers(self):
2010 return (self.GetIssueProperties() or {}).get('reviewers')
2011
Aaron Gable636b13f2017-07-14 10:42:48 -07002012 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002013 return self.RpcServer().add_comment(self.GetIssue(), message)
2014
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002015 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002016 summary = []
2017 for message in self.GetIssueProperties().get('messages', []):
2018 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2019 summary.append(_CommentSummary(
2020 date=date,
2021 disapproval=bool(message['disapproval']),
2022 approval=bool(message['approval']),
2023 sender=message['sender'],
2024 message=message['text'],
2025 ))
2026 return summary
2027
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002028 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002029 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002030 or CQ status, assuming adherence to a common workflow.
2031
2032 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002033 * 'error' - error from review tool (including deleted issues)
2034 * 'unsent' - not sent for review
2035 * 'waiting' - waiting for review
2036 * 'reply' - waiting for owner to reply to review
2037 * 'not lgtm' - Code-Review label has been set negatively
2038 * 'lgtm' - LGTM from at least one approved reviewer
2039 * 'commit' - in the commit queue
2040 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002041 """
2042 if not self.GetIssue():
2043 return None
2044
2045 try:
2046 props = self.GetIssueProperties()
2047 except urllib2.HTTPError:
2048 return 'error'
2049
2050 if props.get('closed'):
2051 # Issue is closed.
2052 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002053 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002054 # Issue is in the commit queue.
2055 return 'commit'
2056
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002057 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002058 if not messages:
2059 # No message was sent.
2060 return 'unsent'
2061
2062 if get_approving_reviewers(props):
2063 return 'lgtm'
2064 elif get_approving_reviewers(props, disapproval=True):
2065 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002066
tandrii9d2c7a32016-06-22 03:42:45 -07002067 # Skip CQ messages that don't require owner's action.
2068 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2069 if 'Dry run:' in messages[-1]['text']:
2070 messages.pop()
2071 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2072 # This message always follows prior messages from CQ,
2073 # so skip this too.
2074 messages.pop()
2075 else:
2076 # This is probably a CQ messages warranting user attention.
2077 break
2078
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002079 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002080 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002081 return 'reply'
2082 return 'waiting'
2083
dsansomee2d6fd92016-09-08 00:10:47 -07002084 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002085 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002088 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002090 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002091 return self.SetFlags({flag: value})
2092
2093 def SetFlags(self, flags):
2094 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002095 """
phajdan.jr68598232016-08-10 03:28:28 -07002096 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002097 try:
tandrii4b233bd2016-07-06 03:50:29 -07002098 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002099 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002100 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002101 if e.code == 404:
2102 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2103 if e.code == 403:
2104 DieWithError(
2105 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002106 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002107 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002109 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110 """Returns an upload.RpcServer() to access this review's rietveld instance.
2111 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002112 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002113 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002114 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002115 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002116 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002118 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002119 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002120 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121
tandrii5d48c322016-08-18 16:19:37 -07002122 @classmethod
2123 def PatchsetConfigKey(cls):
2124 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125
tandrii5d48c322016-08-18 16:19:37 -07002126 @classmethod
2127 def CodereviewServerConfigKey(cls):
2128 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002129
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002130 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002131 return self.RpcServer()
2132
Ravi Mistry31e7d562018-04-02 12:53:57 -04002133 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2134 raise NotImplementedError()
2135
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002136 def SetCQState(self, new_state):
2137 props = self.GetIssueProperties()
2138 if props.get('private'):
2139 DieWithError('Cannot set-commit on private issue')
2140
2141 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002142 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002143 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002144 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002145 else:
tandrii4b233bd2016-07-06 03:50:29 -07002146 assert new_state == _CQState.DRY_RUN
2147 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002148
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002149 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002150 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 # PatchIssue should never be called with a dirty tree. It is up to the
2152 # caller to check this, but just in case we assert here since the
2153 # consequences of the caller not checking this could be dire.
2154 assert(not git_common.is_dirty_git_tree('apply'))
2155 assert(parsed_issue_arg.valid)
2156 self._changelist.issue = parsed_issue_arg.issue
2157 if parsed_issue_arg.hostname:
2158 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2159
skobes6468b902016-10-24 08:45:10 -07002160 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2161 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2162 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002163 try:
skobes6468b902016-10-24 08:45:10 -07002164 scm_obj.apply_patch(patchset_object)
2165 except Exception as e:
2166 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167 return 1
2168
2169 # If we had an issue, commit the current state and register the issue.
2170 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002171 self.SetIssue(self.GetIssue())
2172 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2174 'patch from issue %(i)s at patchset '
2175 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2176 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002177 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002178 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002179 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 return 0
2181
2182 @staticmethod
2183 def ParseIssueURL(parsed_url):
2184 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2185 return None
wychen3c1c1722016-08-04 11:46:36 -07002186 # Rietveld patch: https://domain/<number>/#ps<patchset>
2187 match = re.match(r'/(\d+)/$', parsed_url.path)
2188 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2189 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002190 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002191 issue=int(match.group(1)),
2192 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002193 hostname=parsed_url.netloc,
2194 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 # Typical url: https://domain/<issue_number>[/[other]]
2196 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2197 if match:
skobes6468b902016-10-24 08:45:10 -07002198 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002199 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002200 hostname=parsed_url.netloc,
2201 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2203 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2204 if match:
skobes6468b902016-10-24 08:45:10 -07002205 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002206 issue=int(match.group(1)),
2207 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002208 hostname=parsed_url.netloc,
2209 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002210 return None
2211
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002212 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 """Upload the patch to Rietveld."""
2214 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2215 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2217 if options.emulate_svn_auto_props:
2218 upload_args.append('--emulate_svn_auto_props')
2219
2220 change_desc = None
2221
2222 if options.email is not None:
2223 upload_args.extend(['--email', options.email])
2224
2225 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002226 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 upload_args.extend(['--title', options.title])
2228 if options.message:
2229 upload_args.extend(['--message', options.message])
2230 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002231 print('This branch is associated with issue %s. '
2232 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 else:
nodirca166002016-06-27 10:59:51 -07002234 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002235 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002236 if options.message:
2237 message = options.message
2238 else:
2239 message = CreateDescriptionFromLog(args)
2240 if options.title:
2241 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002243 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002244 change_desc.update_reviewers(options.reviewers, options.tbrs,
2245 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002246 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002247 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248
2249 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002250 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 return 1
2252
2253 upload_args.extend(['--message', change_desc.description])
2254 if change_desc.get_reviewers():
2255 upload_args.append('--reviewers=%s' % ','.join(
2256 change_desc.get_reviewers()))
2257 if options.send_mail:
2258 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002259 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 upload_args.append('--send_mail')
2261
2262 # We check this before applying rietveld.private assuming that in
2263 # rietveld.cc only addresses which we can send private CLs to are listed
2264 # if rietveld.private is set, and so we should ignore rietveld.cc only
2265 # when --private is specified explicitly on the command line.
2266 if options.private:
2267 logging.warn('rietveld.cc is ignored since private flag is specified. '
2268 'You need to review and add them manually if necessary.')
2269 cc = self.GetCCListWithoutDefault()
2270 else:
2271 cc = self.GetCCList()
2272 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002273 if change_desc.get_cced():
2274 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 if cc:
2276 upload_args.extend(['--cc', cc])
2277
2278 if options.private or settings.GetDefaultPrivateFlag() == "True":
2279 upload_args.append('--private')
2280
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002281 # Include the upstream repo's URL in the change -- this is useful for
2282 # projects that have their source spread across multiple repos.
2283 remote_url = self.GetGitBaseUrlFromConfig()
2284 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002285 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2286 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2287 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002288 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002289 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002290 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 if target_ref:
2292 upload_args.extend(['--target_ref', target_ref])
2293
2294 # Look for dependent patchsets. See crbug.com/480453 for more details.
2295 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2296 upstream_branch = ShortBranchName(upstream_branch)
2297 if remote is '.':
2298 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002299 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002300 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002301 print()
2302 print('Skipping dependency patchset upload because git config '
2303 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2304 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305 else:
2306 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002307 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002308 auth_config=auth_config)
2309 branch_cl_issue_url = branch_cl.GetIssueURL()
2310 branch_cl_issue = branch_cl.GetIssue()
2311 branch_cl_patchset = branch_cl.GetPatchset()
2312 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2313 upload_args.extend(
2314 ['--depends_on_patchset', '%s:%s' % (
2315 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002316 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002317 '\n'
2318 'The current branch (%s) is tracking a local branch (%s) with '
2319 'an associated CL.\n'
2320 'Adding %s/#ps%s as a dependency patchset.\n'
2321 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2322 branch_cl_patchset))
2323
2324 project = settings.GetProject()
2325 if project:
2326 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002327 else:
2328 print()
2329 print('WARNING: Uploading without a project specified. Please ensure '
2330 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2331 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002332
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002333 try:
2334 upload_args = ['upload'] + upload_args + args
2335 logging.info('upload.RealMain(%s)', upload_args)
2336 issue, patchset = upload.RealMain(upload_args)
2337 issue = int(issue)
2338 patchset = int(patchset)
2339 except KeyboardInterrupt:
2340 sys.exit(1)
2341 except:
2342 # If we got an exception after the user typed a description for their
2343 # change, back up the description before re-raising.
2344 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002345 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002346 raise
2347
2348 if not self.GetIssue():
2349 self.SetIssue(issue)
2350 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002351 return 0
2352
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002353
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002354class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002355 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002356 # auth_config is Rietveld thing, kept here to preserve interface only.
2357 super(_GerritChangelistImpl, self).__init__(changelist)
2358 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002359 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002360 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002361 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002362 # Map from change number (issue) to its detail cache.
2363 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002364
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002365 if codereview_host is not None:
2366 assert not codereview_host.startswith('https://'), codereview_host
2367 self._gerrit_host = codereview_host
2368 self._gerrit_server = 'https://%s' % codereview_host
2369
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002370 def _GetGerritHost(self):
2371 # Lazy load of configs.
2372 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002373 if self._gerrit_host and '.' not in self._gerrit_host:
2374 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2375 # This happens for internal stuff http://crbug.com/614312.
2376 parsed = urlparse.urlparse(self.GetRemoteUrl())
2377 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002378 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002379 ' Your current remote is: %s' % self.GetRemoteUrl())
2380 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2381 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002382 return self._gerrit_host
2383
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002384 def _GetGitHost(self):
2385 """Returns git host to be used when uploading change to Gerrit."""
2386 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2387
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002388 def GetCodereviewServer(self):
2389 if not self._gerrit_server:
2390 # If we're on a branch then get the server potentially associated
2391 # with that branch.
2392 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002393 self._gerrit_server = self._GitGetBranchConfigValue(
2394 self.CodereviewServerConfigKey())
2395 if self._gerrit_server:
2396 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002397 if not self._gerrit_server:
2398 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2399 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002400 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 parts[0] = parts[0] + '-review'
2402 self._gerrit_host = '.'.join(parts)
2403 self._gerrit_server = 'https://%s' % self._gerrit_host
2404 return self._gerrit_server
2405
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002406 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002407 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002408 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002409
tandrii5d48c322016-08-18 16:19:37 -07002410 @classmethod
2411 def PatchsetConfigKey(cls):
2412 return 'gerritpatchset'
2413
2414 @classmethod
2415 def CodereviewServerConfigKey(cls):
2416 return 'gerritserver'
2417
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002418 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002419 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002420 if settings.GetGerritSkipEnsureAuthenticated():
2421 # For projects with unusual authentication schemes.
2422 # See http://crbug.com/603378.
2423 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002424 # Lazy-loader to identify Gerrit and Git hosts.
2425 if gerrit_util.GceAuthenticator.is_gce():
2426 return
2427 self.GetCodereviewServer()
2428 git_host = self._GetGitHost()
2429 assert self._gerrit_server and self._gerrit_host
2430 cookie_auth = gerrit_util.CookiesAuthenticator()
2431
2432 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2433 git_auth = cookie_auth.get_auth_header(git_host)
2434 if gerrit_auth and git_auth:
2435 if gerrit_auth == git_auth:
2436 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002437 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002438 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002439 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002440 ' %s\n'
2441 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002442 ' Consider running the following command:\n'
2443 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002444 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002445 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002446 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002447 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002448 cookie_auth.get_new_password_message(git_host)))
2449 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002450 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002451 return
2452 else:
2453 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002454 ([] if gerrit_auth else [self._gerrit_host]) +
2455 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002456 DieWithError('Credentials for the following hosts are required:\n'
2457 ' %s\n'
2458 'These are read from %s (or legacy %s)\n'
2459 '%s' % (
2460 '\n '.join(missing),
2461 cookie_auth.get_gitcookies_path(),
2462 cookie_auth.get_netrc_path(),
2463 cookie_auth.get_new_password_message(git_host)))
2464
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002465 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002466 if not self.GetIssue():
2467 return
2468
2469 # Warm change details cache now to avoid RPCs later, reducing latency for
2470 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002471 self._GetChangeDetail(
2472 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002473
2474 status = self._GetChangeDetail()['status']
2475 if status in ('MERGED', 'ABANDONED'):
2476 DieWithError('Change %s has been %s, new uploads are not allowed' %
2477 (self.GetIssueURL(),
2478 'submitted' if status == 'MERGED' else 'abandoned'))
2479
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002480 if gerrit_util.GceAuthenticator.is_gce():
2481 return
2482 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2483 self._GetGerritHost())
2484 if self.GetIssueOwner() == cookies_user:
2485 return
2486 logging.debug('change %s owner is %s, cookies user is %s',
2487 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002488 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002489 # so ask what Gerrit thinks of this user.
2490 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2491 if details['email'] == self.GetIssueOwner():
2492 return
2493 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002494 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002495 'as %s.\n'
2496 'Uploading may fail due to lack of permissions.' %
2497 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2498 confirm_or_exit(action='upload')
2499
2500
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002501 def _PostUnsetIssueProperties(self):
2502 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002503 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002504
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002505 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002506 class ThisIsNotRietveldIssue(object):
2507 def __nonzero__(self):
2508 # This is a hack to make presubmit_support think that rietveld is not
2509 # defined, yet still ensure that calls directly result in a decent
2510 # exception message below.
2511 return False
2512
2513 def __getattr__(self, attr):
2514 print(
2515 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2516 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002517 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002518 'or use Rietveld for codereview.\n'
2519 'See also http://crbug.com/579160.' % attr)
2520 raise NotImplementedError()
2521 return ThisIsNotRietveldIssue()
2522
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002523 def GetGerritObjForPresubmit(self):
2524 return presubmit_support.GerritAccessor(self._GetGerritHost())
2525
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002526 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002527 """Apply a rough heuristic to give a simple summary of an issue's review
2528 or CQ status, assuming adherence to a common workflow.
2529
2530 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002531 * 'error' - error from review tool (including deleted issues)
2532 * 'unsent' - no reviewers added
2533 * 'waiting' - waiting for review
2534 * 'reply' - waiting for uploader to reply to review
2535 * 'lgtm' - Code-Review label has been set
2536 * 'commit' - in the commit queue
2537 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002538 """
2539 if not self.GetIssue():
2540 return None
2541
2542 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002543 data = self._GetChangeDetail([
2544 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002545 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002546 return 'error'
2547
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002548 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002549 return 'closed'
2550
Aaron Gable9ab38c62017-04-06 14:36:33 -07002551 if data['labels'].get('Commit-Queue', {}).get('approved'):
2552 # The section will have an "approved" subsection if anyone has voted
2553 # the maximum value on the label.
2554 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002555
Aaron Gable9ab38c62017-04-06 14:36:33 -07002556 if data['labels'].get('Code-Review', {}).get('approved'):
2557 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002558
2559 if not data.get('reviewers', {}).get('REVIEWER', []):
2560 return 'unsent'
2561
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002562 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002563 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2564 last_message_author = messages.pop().get('author', {})
2565 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002566 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2567 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002568 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002569 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002570 if last_message_author.get('_account_id') == owner:
2571 # Most recent message was by owner.
2572 return 'waiting'
2573 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002574 # Some reply from non-owner.
2575 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002576
2577 # Somehow there are no messages even though there are reviewers.
2578 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002579
2580 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002581 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002582 patchset = data['revisions'][data['current_revision']]['_number']
2583 self.SetPatchset(patchset)
2584 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002585
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002586 def FetchDescription(self, force=False):
2587 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2588 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002589 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002590 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002591
dsansomee2d6fd92016-09-08 00:10:47 -07002592 def UpdateDescriptionRemote(self, description, force=False):
2593 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2594 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002595 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002596 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002597 'unpublished edit. Either publish the edit in the Gerrit web UI '
2598 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002599
2600 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2601 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002602 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002603 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002604
Aaron Gable636b13f2017-07-14 10:42:48 -07002605 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002606 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002607 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002608
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002609 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002610 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002611 messages = self._GetChangeDetail(
2612 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2613 file_comments = gerrit_util.GetChangeComments(
2614 self._GetGerritHost(), self.GetIssue())
2615
2616 # Build dictionary of file comments for easy access and sorting later.
2617 # {author+date: {path: {patchset: {line: url+message}}}}
2618 comments = collections.defaultdict(
2619 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2620 for path, line_comments in file_comments.iteritems():
2621 for comment in line_comments:
2622 if comment.get('tag', '').startswith('autogenerated'):
2623 continue
2624 key = (comment['author']['email'], comment['updated'])
2625 if comment.get('side', 'REVISION') == 'PARENT':
2626 patchset = 'Base'
2627 else:
2628 patchset = 'PS%d' % comment['patch_set']
2629 line = comment.get('line', 0)
2630 url = ('https://%s/c/%s/%s/%s#%s%s' %
2631 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2632 'b' if comment.get('side') == 'PARENT' else '',
2633 str(line) if line else ''))
2634 comments[key][path][patchset][line] = (url, comment['message'])
2635
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002636 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002637 for msg in messages:
2638 # Don't bother showing autogenerated messages.
2639 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2640 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002641 # Gerrit spits out nanoseconds.
2642 assert len(msg['date'].split('.')[-1]) == 9
2643 date = datetime.datetime.strptime(msg['date'][:-3],
2644 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002645 message = msg['message']
2646 key = (msg['author']['email'], msg['date'])
2647 if key in comments:
2648 message += '\n'
2649 for path, patchsets in sorted(comments.get(key, {}).items()):
2650 if readable:
2651 message += '\n%s' % path
2652 for patchset, lines in sorted(patchsets.items()):
2653 for line, (url, content) in sorted(lines.items()):
2654 if line:
2655 line_str = 'Line %d' % line
2656 path_str = '%s:%d:' % (path, line)
2657 else:
2658 line_str = 'File comment'
2659 path_str = '%s:0:' % path
2660 if readable:
2661 message += '\n %s, %s: %s' % (patchset, line_str, url)
2662 message += '\n %s\n' % content
2663 else:
2664 message += '\n%s ' % path_str
2665 message += '\n%s\n' % content
2666
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002667 summary.append(_CommentSummary(
2668 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002669 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002670 sender=msg['author']['email'],
2671 # These could be inferred from the text messages and correlated with
2672 # Code-Review label maximum, however this is not reliable.
2673 # Leaving as is until the need arises.
2674 approval=False,
2675 disapproval=False,
2676 ))
2677 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002678
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002679 def CloseIssue(self):
2680 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2681
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002682 def SubmitIssue(self, wait_for_merge=True):
2683 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2684 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002685
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002686 def _GetChangeDetail(self, options=None, issue=None,
2687 no_cache=False):
2688 """Returns details of the issue by querying Gerrit and caching results.
2689
2690 If fresh data is needed, set no_cache=True which will clear cache and
2691 thus new data will be fetched from Gerrit.
2692 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002693 options = options or []
2694 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002695 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002696
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002697 # Optimization to avoid multiple RPCs:
2698 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2699 'CURRENT_COMMIT' not in options):
2700 options.append('CURRENT_COMMIT')
2701
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002702 # Normalize issue and options for consistent keys in cache.
2703 issue = str(issue)
2704 options = [o.upper() for o in options]
2705
2706 # Check in cache first unless no_cache is True.
2707 if no_cache:
2708 self._detail_cache.pop(issue, None)
2709 else:
2710 options_set = frozenset(options)
2711 for cached_options_set, data in self._detail_cache.get(issue, []):
2712 # Assumption: data fetched before with extra options is suitable
2713 # for return for a smaller set of options.
2714 # For example, if we cached data for
2715 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2716 # and request is for options=[CURRENT_REVISION],
2717 # THEN we can return prior cached data.
2718 if options_set.issubset(cached_options_set):
2719 return data
2720
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002721 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002722 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002723 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002724 except gerrit_util.GerritError as e:
2725 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002726 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002727 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002728
2729 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002730 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002731
agable32978d92016-11-01 12:55:02 -07002732 def _GetChangeCommit(self, issue=None):
2733 issue = issue or self.GetIssue()
2734 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002735 try:
2736 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2737 except gerrit_util.GerritError as e:
2738 if e.http_status == 404:
2739 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2740 raise
agable32978d92016-11-01 12:55:02 -07002741 return data
2742
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002743 def CMDLand(self, force, bypass_hooks, verbose):
2744 if git_common.is_dirty_git_tree('land'):
2745 return 1
tandriid60367b2016-06-22 05:25:12 -07002746 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2747 if u'Commit-Queue' in detail.get('labels', {}):
2748 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002749 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2750 'which can test and land changes for you. '
2751 'Are you sure you wish to bypass it?\n',
2752 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002753
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002754 differs = True
tandriic4344b52016-08-29 06:04:54 -07002755 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002756 # Note: git diff outputs nothing if there is no diff.
2757 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002758 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002759 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002760 if detail['current_revision'] == last_upload:
2761 differs = False
2762 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002763 print('WARNING: Local branch contents differ from latest uploaded '
2764 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002765 if differs:
2766 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002767 confirm_or_exit(
2768 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2769 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002770 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002771 elif not bypass_hooks:
2772 hook_results = self.RunHook(
2773 committing=True,
2774 may_prompt=not force,
2775 verbose=verbose,
2776 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2777 if not hook_results.should_continue():
2778 return 1
2779
2780 self.SubmitIssue(wait_for_merge=True)
2781 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002782 links = self._GetChangeCommit().get('web_links', [])
2783 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002784 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002785 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002786 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002787 return 0
2788
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002789 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002790 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002791 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002792 assert not directory
2793 assert parsed_issue_arg.valid
2794
2795 self._changelist.issue = parsed_issue_arg.issue
2796
2797 if parsed_issue_arg.hostname:
2798 self._gerrit_host = parsed_issue_arg.hostname
2799 self._gerrit_server = 'https://%s' % self._gerrit_host
2800
tandriic2405f52016-10-10 08:13:15 -07002801 try:
2802 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002803 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002804 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002805
2806 if not parsed_issue_arg.patchset:
2807 # Use current revision by default.
2808 revision_info = detail['revisions'][detail['current_revision']]
2809 patchset = int(revision_info['_number'])
2810 else:
2811 patchset = parsed_issue_arg.patchset
2812 for revision_info in detail['revisions'].itervalues():
2813 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2814 break
2815 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002816 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002817 (parsed_issue_arg.patchset, self.GetIssue()))
2818
Aaron Gable697a91b2018-01-19 15:20:15 -08002819 remote_url = self._changelist.GetRemoteUrl()
2820 if remote_url.endswith('.git'):
2821 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002822 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002823
2824 if remote_url != fetch_info['url']:
2825 DieWithError('Trying to patch a change from %s but this repo appears '
2826 'to be %s.' % (fetch_info['url'], remote_url))
2827
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002828 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002829
Aaron Gable62619a32017-06-16 08:22:09 -07002830 if force:
2831 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2832 print('Checked out commit for change %i patchset %i locally' %
2833 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002834 elif nocommit:
2835 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2836 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002837 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002838 RunGit(['cherry-pick', 'FETCH_HEAD'])
2839 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002840 (parsed_issue_arg.issue, patchset))
2841 print('Note: this created a local commit which does not have '
2842 'the same hash as the one uploaded for review. This will make '
2843 'uploading changes based on top of this branch difficult.\n'
2844 'If you want to do that, use "git cl patch --force" instead.')
2845
Stefan Zagerd08043c2017-10-12 12:07:02 -07002846 if self.GetBranch():
2847 self.SetIssue(parsed_issue_arg.issue)
2848 self.SetPatchset(patchset)
2849 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2850 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2851 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2852 else:
2853 print('WARNING: You are in detached HEAD state.\n'
2854 'The patch has been applied to your checkout, but you will not be '
2855 'able to upload a new patch set to the gerrit issue.\n'
2856 'Try using the \'-b\' option if you would like to work on a '
2857 'branch and/or upload a new patch set.')
2858
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002859 return 0
2860
2861 @staticmethod
2862 def ParseIssueURL(parsed_url):
2863 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2864 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002865 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2866 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002867 # Short urls like https://domain/<issue_number> can be used, but don't allow
2868 # specifying the patchset (you'd 404), but we allow that here.
2869 if parsed_url.path == '/':
2870 part = parsed_url.fragment
2871 else:
2872 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002873 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002874 if match:
2875 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002876 issue=int(match.group(3)),
2877 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002878 hostname=parsed_url.netloc,
2879 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002880 return None
2881
tandrii16e0b4e2016-06-07 10:34:28 -07002882 def _GerritCommitMsgHookCheck(self, offer_removal):
2883 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2884 if not os.path.exists(hook):
2885 return
2886 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2887 # custom developer made one.
2888 data = gclient_utils.FileRead(hook)
2889 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2890 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002891 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002892 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002893 'and may interfere with it in subtle ways.\n'
2894 'We recommend you remove the commit-msg hook.')
2895 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002896 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002897 gclient_utils.rm_file_or_tree(hook)
2898 print('Gerrit commit-msg hook removed.')
2899 else:
2900 print('OK, will keep Gerrit commit-msg hook in place.')
2901
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002902 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002903 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002904 if options.squash and options.no_squash:
2905 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002906
2907 if not options.squash and not options.no_squash:
2908 # Load default for user, repo, squash=true, in this order.
2909 options.squash = settings.GetSquashGerritUploads()
2910 elif options.no_squash:
2911 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002912
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002913 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002914 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002915
Aaron Gableb56ad332017-01-06 15:24:31 -08002916 # This may be None; default fallback value is determined in logic below.
2917 title = options.title
2918
Dominic Battre7d1c4842017-10-27 09:17:28 +02002919 # Extract bug number from branch name.
2920 bug = options.bug
2921 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2922 if not bug and match:
2923 bug = match.group(1)
2924
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002925 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002926 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002927 if self.GetIssue():
2928 # Try to get the message from a previous upload.
2929 message = self.GetDescription()
2930 if not message:
2931 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002932 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002933 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002934 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002935 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002936 # When uploading a subsequent patchset, -m|--message is taken
2937 # as the patchset title if --title was not provided.
2938 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002939 else:
2940 default_title = RunGit(
2941 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002942 if options.force:
2943 title = default_title
2944 else:
2945 title = ask_for_data(
2946 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002947 change_id = self._GetChangeDetail()['change_id']
2948 while True:
2949 footer_change_ids = git_footers.get_footer_change_id(message)
2950 if footer_change_ids == [change_id]:
2951 break
2952 if not footer_change_ids:
2953 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002954 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002955 continue
2956 # There is already a valid footer but with different or several ids.
2957 # Doing this automatically is non-trivial as we don't want to lose
2958 # existing other footers, yet we want to append just 1 desired
2959 # Change-Id. Thus, just create a new footer, but let user verify the
2960 # new description.
2961 message = '%s\n\nChange-Id: %s' % (message, change_id)
2962 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002963 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002964 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002965 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002966 'Please, check the proposed correction to the description, '
2967 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2968 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2969 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002970 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002971 if not options.force:
2972 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002973 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002974 message = change_desc.description
2975 if not message:
2976 DieWithError("Description is empty. Aborting...")
2977 # Continue the while loop.
2978 # Sanity check of this code - we should end up with proper message
2979 # footer.
2980 assert [change_id] == git_footers.get_footer_change_id(message)
2981 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002982 else: # if not self.GetIssue()
2983 if options.message:
2984 message = options.message
2985 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002986 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002987 if options.title:
2988 message = options.title + '\n\n' + message
2989 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002990
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002991 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002992 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002993 # On first upload, patchset title is always this string, while
2994 # --title flag gets converted to first line of message.
2995 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002996 if not change_desc.description:
2997 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002998 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002999 if len(change_ids) > 1:
3000 DieWithError('too many Change-Id footers, at most 1 allowed.')
3001 if not change_ids:
3002 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003003 change_desc.set_description(git_footers.add_footer_change_id(
3004 change_desc.description,
3005 GenerateGerritChangeId(change_desc.description)))
3006 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003007 assert len(change_ids) == 1
3008 change_id = change_ids[0]
3009
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)
3013
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003014 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003015 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3016 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003017 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003018 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3019 desc_tempfile.write(change_desc.description)
3020 desc_tempfile.close()
3021 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3022 '-F', desc_tempfile.name]).strip()
3023 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003024 else:
3025 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003026 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003027 if not change_desc.description:
3028 DieWithError("Description is empty. Aborting...")
3029
3030 if not git_footers.get_footer_change_id(change_desc.description):
3031 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003032 change_desc.set_description(
3033 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003034 if options.reviewers or options.tbrs or options.add_owners_to:
3035 change_desc.update_reviewers(options.reviewers, options.tbrs,
3036 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003037 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003038 # For no-squash mode, we assume the remote called "origin" is the one we
3039 # want. It is not worthwhile to support different workflows for
3040 # no-squash mode.
3041 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003042 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3043
3044 assert change_desc
3045 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3046 ref_to_push)]).splitlines()
3047 if len(commits) > 1:
3048 print('WARNING: This will upload %d commits. Run the following command '
3049 'to see which commits will be uploaded: ' % len(commits))
3050 print('git log %s..%s' % (parent, ref_to_push))
3051 print('You can also use `git squash-branch` to squash these into a '
3052 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003053 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003054
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003055 if options.reviewers or options.tbrs or options.add_owners_to:
3056 change_desc.update_reviewers(options.reviewers, options.tbrs,
3057 options.add_owners_to, change)
3058
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003059 # Extra options that can be specified at push time. Doc:
3060 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003061 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003062
Aaron Gable844cf292017-06-28 11:32:59 -07003063 # By default, new changes are started in WIP mode, and subsequent patchsets
3064 # don't send email. At any time, passing --send-mail will mark the change
3065 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003066 if options.send_mail:
3067 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003068 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003069 elif not self.GetIssue():
3070 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003071 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003072 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003073
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003074 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003075 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003076
Aaron Gable9b713dd2016-12-14 16:04:21 -08003077 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003078 # Punctuation and whitespace in |title| must be percent-encoded.
3079 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003080
agablec6787972016-09-09 16:13:34 -07003081 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003082 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003083
rmistry9eadede2016-09-19 11:22:43 -07003084 if options.topic:
3085 # Documentation on Gerrit topics is here:
3086 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003087 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003088
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003089 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003090 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003091 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003092 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003093 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3094
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003095 refspec_suffix = ''
3096 if refspec_opts:
3097 refspec_suffix = '%' + ','.join(refspec_opts)
3098 assert ' ' not in refspec_suffix, (
3099 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3100 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3101
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003102 try:
3103 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003104 ['git', 'push', self.GetRemoteUrl(), refspec],
3105 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003106 # Flush after every line: useful for seeing progress when running as
3107 # recipe.
3108 filter_fn=lambda _: sys.stdout.flush())
3109 except subprocess2.CalledProcessError:
3110 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003111 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003112 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003113 'credential problems:\n'
3114 ' git cl creds-check\n',
3115 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003116
3117 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003118 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003119 change_numbers = [m.group(1)
3120 for m in map(regex.match, push_stdout.splitlines())
3121 if m]
3122 if len(change_numbers) != 1:
3123 DieWithError(
3124 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003125 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003126 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003127 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003128
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003129 reviewers = sorted(change_desc.get_reviewers())
3130
tandrii88189772016-09-29 04:29:57 -07003131 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003132 if not options.private:
3133 cc = self.GetCCList().split(',')
3134 else:
3135 cc = []
tandrii88189772016-09-29 04:29:57 -07003136 if options.cc:
3137 cc.extend(options.cc)
3138 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003139 if change_desc.get_cced():
3140 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003141
3142 gerrit_util.AddReviewers(
3143 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3144 notify=bool(options.send_mail))
3145
Aaron Gablefd238082017-06-07 13:42:34 -07003146 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003147 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3148 score = 1
3149 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3150 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3151 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003152 gerrit_util.SetReview(
3153 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003154 msg='Self-approving for TBR',
3155 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003156
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003157 return 0
3158
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003159 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3160 change_desc):
3161 """Computes parent of the generated commit to be uploaded to Gerrit.
3162
3163 Returns revision or a ref name.
3164 """
3165 if custom_cl_base:
3166 # Try to avoid creating additional unintended CLs when uploading, unless
3167 # user wants to take this risk.
3168 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3169 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3170 local_ref_of_target_remote])
3171 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003172 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003173 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3174 'If you proceed with upload, more than 1 CL may be created by '
3175 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3176 'If you are certain that specified base `%s` has already been '
3177 'uploaded to Gerrit as another CL, you may proceed.\n' %
3178 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3179 if not force:
3180 confirm_or_exit(
3181 'Do you take responsibility for cleaning up potential mess '
3182 'resulting from proceeding with upload?',
3183 action='upload')
3184 return custom_cl_base
3185
Aaron Gablef97e33d2017-03-30 15:44:27 -07003186 if remote != '.':
3187 return self.GetCommonAncestorWithUpstream()
3188
3189 # If our upstream branch is local, we base our squashed commit on its
3190 # squashed version.
3191 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3192
Aaron Gablef97e33d2017-03-30 15:44:27 -07003193 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003194 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003195
3196 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003197 # TODO(tandrii): consider checking parent change in Gerrit and using its
3198 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3199 # the tree hash of the parent branch. The upside is less likely bogus
3200 # requests to reupload parent change just because it's uploadhash is
3201 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003202 parent = RunGit(['config',
3203 'branch.%s.gerritsquashhash' % upstream_branch_name],
3204 error_ok=True).strip()
3205 # Verify that the upstream branch has been uploaded too, otherwise
3206 # Gerrit will create additional CLs when uploading.
3207 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3208 RunGitSilent(['rev-parse', parent + ':'])):
3209 DieWithError(
3210 '\nUpload upstream branch %s first.\n'
3211 'It is likely that this branch has been rebased since its last '
3212 'upload, so you just need to upload it again.\n'
3213 '(If you uploaded it with --no-squash, then branch dependencies '
3214 'are not supported, and you should reupload with --squash.)'
3215 % upstream_branch_name,
3216 change_desc)
3217 return parent
3218
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003219 def _AddChangeIdToCommitMessage(self, options, args):
3220 """Re-commits using the current message, assumes the commit hook is in
3221 place.
3222 """
3223 log_desc = options.message or CreateDescriptionFromLog(args)
3224 git_command = ['commit', '--amend', '-m', log_desc]
3225 RunGit(git_command)
3226 new_log_desc = CreateDescriptionFromLog(args)
3227 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003228 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003229 return new_log_desc
3230 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003231 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003232
Ravi Mistry31e7d562018-04-02 12:53:57 -04003233 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3234 """Sets labels on the change based on the provided flags."""
3235 labels = {}
3236 notify = None;
3237 if enable_auto_submit:
3238 labels['Auto-Submit'] = 1
3239 if use_commit_queue:
3240 labels['Commit-Queue'] = 2
3241 elif cq_dry_run:
3242 labels['Commit-Queue'] = 1
3243 notify = False
3244 if labels:
3245 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3246 labels=labels, notify=notify)
3247
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003248 def SetCQState(self, new_state):
3249 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003250 vote_map = {
3251 _CQState.NONE: 0,
3252 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003253 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003254 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003255 labels = {'Commit-Queue': vote_map[new_state]}
3256 notify = False if new_state == _CQState.DRY_RUN else None
3257 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3258 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003259
tandriie113dfd2016-10-11 10:20:12 -07003260 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003261 try:
3262 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003263 except GerritChangeNotExists:
3264 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003265
3266 if data['status'] in ('ABANDONED', 'MERGED'):
3267 return 'CL %s is closed' % self.GetIssue()
3268
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003269 def GetTryJobProperties(self, patchset=None):
3270 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003271 data = self._GetChangeDetail(['ALL_REVISIONS'])
3272 patchset = int(patchset or self.GetPatchset())
3273 assert patchset
3274 revision_data = None # Pylint wants it to be defined.
3275 for revision_data in data['revisions'].itervalues():
3276 if int(revision_data['_number']) == patchset:
3277 break
3278 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003279 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003280 (patchset, self.GetIssue()))
3281 return {
3282 'patch_issue': self.GetIssue(),
3283 'patch_set': patchset or self.GetPatchset(),
3284 'patch_project': data['project'],
3285 'patch_storage': 'gerrit',
3286 'patch_ref': revision_data['fetch']['http']['ref'],
3287 'patch_repository_url': revision_data['fetch']['http']['url'],
3288 'patch_gerrit_url': self.GetCodereviewServer(),
3289 }
tandriie113dfd2016-10-11 10:20:12 -07003290
tandriide281ae2016-10-12 06:02:30 -07003291 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003292 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003293
Edward Lemur707d70b2018-02-07 00:50:14 +01003294 def GetReviewers(self):
3295 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3296 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3297
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003298
3299_CODEREVIEW_IMPLEMENTATIONS = {
3300 'rietveld': _RietveldChangelistImpl,
3301 'gerrit': _GerritChangelistImpl,
3302}
3303
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003304
iannuccie53c9352016-08-17 14:40:40 -07003305def _add_codereview_issue_select_options(parser, extra=""):
3306 _add_codereview_select_options(parser)
3307
3308 text = ('Operate on this issue number instead of the current branch\'s '
3309 'implicit issue.')
3310 if extra:
3311 text += ' '+extra
3312 parser.add_option('-i', '--issue', type=int, help=text)
3313
3314
3315def _process_codereview_issue_select_options(parser, options):
3316 _process_codereview_select_options(parser, options)
3317 if options.issue is not None and not options.forced_codereview:
3318 parser.error('--issue must be specified with either --rietveld or --gerrit')
3319
3320
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003321def _add_codereview_select_options(parser):
3322 """Appends --gerrit and --rietveld options to force specific codereview."""
3323 parser.codereview_group = optparse.OptionGroup(
3324 parser, 'EXPERIMENTAL! Codereview override options')
3325 parser.add_option_group(parser.codereview_group)
3326 parser.codereview_group.add_option(
3327 '--gerrit', action='store_true',
3328 help='Force the use of Gerrit for codereview')
3329 parser.codereview_group.add_option(
3330 '--rietveld', action='store_true',
3331 help='Force the use of Rietveld for codereview')
3332
3333
3334def _process_codereview_select_options(parser, options):
3335 if options.gerrit and options.rietveld:
3336 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3337 options.forced_codereview = None
3338 if options.gerrit:
3339 options.forced_codereview = 'gerrit'
3340 elif options.rietveld:
3341 options.forced_codereview = 'rietveld'
3342
3343
tandriif9aefb72016-07-01 09:06:51 -07003344def _get_bug_line_values(default_project, bugs):
3345 """Given default_project and comma separated list of bugs, yields bug line
3346 values.
3347
3348 Each bug can be either:
3349 * a number, which is combined with default_project
3350 * string, which is left as is.
3351
3352 This function may produce more than one line, because bugdroid expects one
3353 project per line.
3354
3355 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3356 ['v8:123', 'chromium:789']
3357 """
3358 default_bugs = []
3359 others = []
3360 for bug in bugs.split(','):
3361 bug = bug.strip()
3362 if bug:
3363 try:
3364 default_bugs.append(int(bug))
3365 except ValueError:
3366 others.append(bug)
3367
3368 if default_bugs:
3369 default_bugs = ','.join(map(str, default_bugs))
3370 if default_project:
3371 yield '%s:%s' % (default_project, default_bugs)
3372 else:
3373 yield default_bugs
3374 for other in sorted(others):
3375 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3376 yield other
3377
3378
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003379class ChangeDescription(object):
3380 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003381 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003382 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003383 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003384 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003385 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3386 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3387 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3388 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003389
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003390 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003391 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003392
agable@chromium.org42c20792013-09-12 17:34:49 +00003393 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003394 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003395 return '\n'.join(self._description_lines)
3396
3397 def set_description(self, desc):
3398 if isinstance(desc, basestring):
3399 lines = desc.splitlines()
3400 else:
3401 lines = [line.rstrip() for line in desc]
3402 while lines and not lines[0]:
3403 lines.pop(0)
3404 while lines and not lines[-1]:
3405 lines.pop(-1)
3406 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003407
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003408 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3409 """Rewrites the R=/TBR= line(s) as a single line each.
3410
3411 Args:
3412 reviewers (list(str)) - list of additional emails to use for reviewers.
3413 tbrs (list(str)) - list of additional emails to use for TBRs.
3414 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3415 the change that are missing OWNER coverage. If this is not None, you
3416 must also pass a value for `change`.
3417 change (Change) - The Change that should be used for OWNERS lookups.
3418 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003419 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003420 assert isinstance(tbrs, list), tbrs
3421
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003422 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003423 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003424
3425 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003426 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003427
3428 reviewers = set(reviewers)
3429 tbrs = set(tbrs)
3430 LOOKUP = {
3431 'TBR': tbrs,
3432 'R': reviewers,
3433 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003434
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003435 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003436 regexp = re.compile(self.R_LINE)
3437 matches = [regexp.match(line) for line in self._description_lines]
3438 new_desc = [l for i, l in enumerate(self._description_lines)
3439 if not matches[i]]
3440 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003441
agable@chromium.org42c20792013-09-12 17:34:49 +00003442 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003443
3444 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003445 for match in matches:
3446 if not match:
3447 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003448 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3449
3450 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003451 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003452 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003453 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003454 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003455 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003456 LOOKUP[add_owners_to].update(
3457 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003458
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003459 # If any folks ended up in both groups, remove them from tbrs.
3460 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003461
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003462 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3463 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003464
3465 # Put the new lines in the description where the old first R= line was.
3466 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3467 if 0 <= line_loc < len(self._description_lines):
3468 if new_tbr_line:
3469 self._description_lines.insert(line_loc, new_tbr_line)
3470 if new_r_line:
3471 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003472 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003473 if new_r_line:
3474 self.append_footer(new_r_line)
3475 if new_tbr_line:
3476 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003477
Aaron Gable3a16ed12017-03-23 10:51:55 -07003478 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003479 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003480 self.set_description([
3481 '# Enter a description of the change.',
3482 '# This will be displayed on the codereview site.',
3483 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003484 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003485 '--------------------',
3486 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003487
agable@chromium.org42c20792013-09-12 17:34:49 +00003488 regexp = re.compile(self.BUG_LINE)
3489 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003490 prefix = settings.GetBugPrefix()
3491 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003492 if git_footer:
3493 self.append_footer('Bug: %s' % ', '.join(values))
3494 else:
3495 for value in values:
3496 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003497
agable@chromium.org42c20792013-09-12 17:34:49 +00003498 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003499 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003500 if not content:
3501 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003502 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003503
Bruce Dawson2377b012018-01-11 16:46:49 -08003504 # Strip off comments and default inserted "Bug:" line.
3505 clean_lines = [line.rstrip() for line in lines if not
3506 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003507 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003508 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003509 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003510
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003511 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003512 """Adds a footer line to the description.
3513
3514 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3515 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3516 that Gerrit footers are always at the end.
3517 """
3518 parsed_footer_line = git_footers.parse_footer(line)
3519 if parsed_footer_line:
3520 # Line is a gerrit footer in the form: Footer-Key: any value.
3521 # Thus, must be appended observing Gerrit footer rules.
3522 self.set_description(
3523 git_footers.add_footer(self.description,
3524 key=parsed_footer_line[0],
3525 value=parsed_footer_line[1]))
3526 return
3527
3528 if not self._description_lines:
3529 self._description_lines.append(line)
3530 return
3531
3532 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3533 if gerrit_footers:
3534 # git_footers.split_footers ensures that there is an empty line before
3535 # actual (gerrit) footers, if any. We have to keep it that way.
3536 assert top_lines and top_lines[-1] == ''
3537 top_lines, separator = top_lines[:-1], top_lines[-1:]
3538 else:
3539 separator = [] # No need for separator if there are no gerrit_footers.
3540
3541 prev_line = top_lines[-1] if top_lines else ''
3542 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3543 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3544 top_lines.append('')
3545 top_lines.append(line)
3546 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003547
tandrii99a72f22016-08-17 14:33:24 -07003548 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003549 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003550 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003551 reviewers = [match.group(2).strip()
3552 for match in matches
3553 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003554 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003555
bradnelsond975b302016-10-23 12:20:23 -07003556 def get_cced(self):
3557 """Retrieves the list of reviewers."""
3558 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3559 cced = [match.group(2).strip() for match in matches if match]
3560 return cleanup_list(cced)
3561
Nodir Turakulov23b82142017-11-16 11:04:25 -08003562 def get_hash_tags(self):
3563 """Extracts and sanitizes a list of Gerrit hashtags."""
3564 subject = (self._description_lines or ('',))[0]
3565 subject = re.sub(
3566 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3567
3568 tags = []
3569 start = 0
3570 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3571 while True:
3572 m = bracket_exp.match(subject, start)
3573 if not m:
3574 break
3575 tags.append(self.sanitize_hash_tag(m.group(1)))
3576 start = m.end()
3577
3578 if not tags:
3579 # Try "Tag: " prefix.
3580 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3581 if m:
3582 tags.append(self.sanitize_hash_tag(m.group(1)))
3583 return tags
3584
3585 @classmethod
3586 def sanitize_hash_tag(cls, tag):
3587 """Returns a sanitized Gerrit hash tag.
3588
3589 A sanitized hashtag can be used as a git push refspec parameter value.
3590 """
3591 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3592
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003593 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3594 """Updates this commit description given the parent.
3595
3596 This is essentially what Gnumbd used to do.
3597 Consult https://goo.gl/WMmpDe for more details.
3598 """
3599 assert parent_msg # No, orphan branch creation isn't supported.
3600 assert parent_hash
3601 assert dest_ref
3602 parent_footer_map = git_footers.parse_footers(parent_msg)
3603 # This will also happily parse svn-position, which GnumbD is no longer
3604 # supporting. While we'd generate correct footers, the verifier plugin
3605 # installed in Gerrit will block such commit (ie git push below will fail).
3606 parent_position = git_footers.get_position(parent_footer_map)
3607
3608 # Cherry-picks may have last line obscuring their prior footers,
3609 # from git_footers perspective. This is also what Gnumbd did.
3610 cp_line = None
3611 if (self._description_lines and
3612 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3613 cp_line = self._description_lines.pop()
3614
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003615 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003616
3617 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3618 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003619 for i, line in enumerate(footer_lines):
3620 k, v = git_footers.parse_footer(line) or (None, None)
3621 if k and k.startswith('Cr-'):
3622 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003623
3624 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003625 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003626 if parent_position[0] == dest_ref:
3627 # Same branch as parent.
3628 number = int(parent_position[1]) + 1
3629 else:
3630 number = 1 # New branch, and extra lineage.
3631 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3632 int(parent_position[1])))
3633
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003634 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3635 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003636
3637 self._description_lines = top_lines
3638 if cp_line:
3639 self._description_lines.append(cp_line)
3640 if self._description_lines[-1] != '':
3641 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003642 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003643
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003644
Aaron Gablea1bab272017-04-11 16:38:18 -07003645def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003646 """Retrieves the reviewers that approved a CL from the issue properties with
3647 messages.
3648
3649 Note that the list may contain reviewers that are not committer, thus are not
3650 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003651
3652 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003653 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003654 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003655 return sorted(
3656 set(
3657 message['sender']
3658 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003659 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003660 )
3661 )
3662
3663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664def FindCodereviewSettingsFile(filename='codereview.settings'):
3665 """Finds the given file starting in the cwd and going up.
3666
3667 Only looks up to the top of the repository unless an
3668 'inherit-review-settings-ok' file exists in the root of the repository.
3669 """
3670 inherit_ok_file = 'inherit-review-settings-ok'
3671 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003672 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003673 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3674 root = '/'
3675 while True:
3676 if filename in os.listdir(cwd):
3677 if os.path.isfile(os.path.join(cwd, filename)):
3678 return open(os.path.join(cwd, filename))
3679 if cwd == root:
3680 break
3681 cwd = os.path.dirname(cwd)
3682
3683
3684def LoadCodereviewSettingsFromFile(fileobj):
3685 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003686 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003688 def SetProperty(name, setting, unset_error_ok=False):
3689 fullname = 'rietveld.' + name
3690 if setting in keyvals:
3691 RunGit(['config', fullname, keyvals[setting]])
3692 else:
3693 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3694
tandrii48df5812016-10-17 03:55:37 -07003695 if not keyvals.get('GERRIT_HOST', False):
3696 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003697 # Only server setting is required. Other settings can be absent.
3698 # In that case, we ignore errors raised during option deletion attempt.
3699 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003700 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3702 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003703 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003704 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3705 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003706 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003707 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3708 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003710 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003711 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003712
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003713 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003714 RunGit(['config', 'gerrit.squash-uploads',
3715 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003716
tandrii@chromium.org28253532016-04-14 13:46:56 +00003717 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003718 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003719 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003722 # should be of the form
3723 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3724 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3726 keyvals['ORIGIN_URL_CONFIG']])
3727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003729def urlretrieve(source, destination):
3730 """urllib is broken for SSL connections via a proxy therefore we
3731 can't use urllib.urlretrieve()."""
3732 with open(destination, 'w') as f:
3733 f.write(urllib2.urlopen(source).read())
3734
3735
ukai@chromium.org712d6102013-11-27 00:52:58 +00003736def hasSheBang(fname):
3737 """Checks fname is a #! script."""
3738 with open(fname) as f:
3739 return f.read(2).startswith('#!')
3740
3741
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003742# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3743def DownloadHooks(*args, **kwargs):
3744 pass
3745
3746
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003747def DownloadGerritHook(force):
3748 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003749
3750 Args:
3751 force: True to update hooks. False to install hooks if not present.
3752 """
3753 if not settings.GetIsGerrit():
3754 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003755 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003756 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3757 if not os.access(dst, os.X_OK):
3758 if os.path.exists(dst):
3759 if not force:
3760 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003761 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003762 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003763 if not hasSheBang(dst):
3764 DieWithError('Not a script: %s\n'
3765 'You need to download from\n%s\n'
3766 'into .git/hooks/commit-msg and '
3767 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003768 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3769 except Exception:
3770 if os.path.exists(dst):
3771 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003772 DieWithError('\nFailed to download hooks.\n'
3773 'You need to download from\n%s\n'
3774 'into .git/hooks/commit-msg and '
3775 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003776
3777
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003778def GetRietveldCodereviewSettingsInteractively():
3779 """Prompt the user for settings."""
3780 server = settings.GetDefaultServerUrl(error_ok=True)
3781 prompt = 'Rietveld server (host[:port])'
3782 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3783 newserver = ask_for_data(prompt + ':')
3784 if not server and not newserver:
3785 newserver = DEFAULT_SERVER
3786 if newserver:
3787 newserver = gclient_utils.UpgradeToHttps(newserver)
3788 if newserver != server:
3789 RunGit(['config', 'rietveld.server', newserver])
3790
3791 def SetProperty(initial, caption, name, is_url):
3792 prompt = caption
3793 if initial:
3794 prompt += ' ("x" to clear) [%s]' % initial
3795 new_val = ask_for_data(prompt + ':')
3796 if new_val == 'x':
3797 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3798 elif new_val:
3799 if is_url:
3800 new_val = gclient_utils.UpgradeToHttps(new_val)
3801 if new_val != initial:
3802 RunGit(['config', 'rietveld.' + name, new_val])
3803
3804 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3805 SetProperty(settings.GetDefaultPrivateFlag(),
3806 'Private flag (rietveld only)', 'private', False)
3807 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3808 'tree-status-url', False)
3809 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3810 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3811 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3812 'run-post-upload-hook', False)
3813
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003814
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003815class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003816 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003817
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003818 _GOOGLESOURCE = 'googlesource.com'
3819
3820 def __init__(self):
3821 # Cached list of [host, identity, source], where source is either
3822 # .gitcookies or .netrc.
3823 self._all_hosts = None
3824
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003825 def ensure_configured_gitcookies(self):
3826 """Runs checks and suggests fixes to make git use .gitcookies from default
3827 path."""
3828 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3829 configured_path = RunGitSilent(
3830 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003831 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003832 if configured_path:
3833 self._ensure_default_gitcookies_path(configured_path, default)
3834 else:
3835 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003836
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003837 @staticmethod
3838 def _ensure_default_gitcookies_path(configured_path, default_path):
3839 assert configured_path
3840 if configured_path == default_path:
3841 print('git is already configured to use your .gitcookies from %s' %
3842 configured_path)
3843 return
3844
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003845 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003846 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3847 (configured_path, default_path))
3848
3849 if not os.path.exists(configured_path):
3850 print('However, your configured .gitcookies file is missing.')
3851 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3852 action='reconfigure')
3853 RunGit(['config', '--global', 'http.cookiefile', default_path])
3854 return
3855
3856 if os.path.exists(default_path):
3857 print('WARNING: default .gitcookies file already exists %s' %
3858 default_path)
3859 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3860 default_path)
3861
3862 confirm_or_exit('Move existing .gitcookies to default location?',
3863 action='move')
3864 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003865 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003866 print('Moved and reconfigured git to use .gitcookies from %s' %
3867 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003868
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003869 @staticmethod
3870 def _configure_gitcookies_path(default_path):
3871 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3872 if os.path.exists(netrc_path):
3873 print('You seem to be using outdated .netrc for git credentials: %s' %
3874 netrc_path)
3875 print('This tool will guide you through setting up recommended '
3876 '.gitcookies store for git credentials.\n'
3877 '\n'
3878 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3879 ' git config --global --unset http.cookiefile\n'
3880 ' mv %s %s.backup\n\n' % (default_path, default_path))
3881 confirm_or_exit(action='setup .gitcookies')
3882 RunGit(['config', '--global', 'http.cookiefile', default_path])
3883 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003884
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003885 def get_hosts_with_creds(self, include_netrc=False):
3886 if self._all_hosts is None:
3887 a = gerrit_util.CookiesAuthenticator()
3888 self._all_hosts = [
3889 (h, u, s)
3890 for h, u, s in itertools.chain(
3891 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3892 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3893 )
3894 if h.endswith(self._GOOGLESOURCE)
3895 ]
3896
3897 if include_netrc:
3898 return self._all_hosts
3899 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3900
3901 def print_current_creds(self, include_netrc=False):
3902 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3903 if not hosts:
3904 print('No Git/Gerrit credentials found')
3905 return
3906 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3907 header = [('Host', 'User', 'Which file'),
3908 ['=' * l for l in lengths]]
3909 for row in (header + hosts):
3910 print('\t'.join((('%%+%ds' % l) % s)
3911 for l, s in zip(lengths, row)))
3912
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003913 @staticmethod
3914 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003915 """Parses identity "git-<username>.domain" into <username> and domain."""
3916 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003917 # distinguishable from sub-domains. But we do know typical domains:
3918 if identity.endswith('.chromium.org'):
3919 domain = 'chromium.org'
3920 username = identity[:-len('.chromium.org')]
3921 else:
3922 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003923 if username.startswith('git-'):
3924 username = username[len('git-'):]
3925 return username, domain
3926
3927 def _get_usernames_of_domain(self, domain):
3928 """Returns list of usernames referenced by .gitcookies in a given domain."""
3929 identities_by_domain = {}
3930 for _, identity, _ in self.get_hosts_with_creds():
3931 username, domain = self._parse_identity(identity)
3932 identities_by_domain.setdefault(domain, []).append(username)
3933 return identities_by_domain.get(domain)
3934
3935 def _canonical_git_googlesource_host(self, host):
3936 """Normalizes Gerrit hosts (with '-review') to Git host."""
3937 assert host.endswith(self._GOOGLESOURCE)
3938 # Prefix doesn't include '.' at the end.
3939 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3940 if prefix.endswith('-review'):
3941 prefix = prefix[:-len('-review')]
3942 return prefix + '.' + self._GOOGLESOURCE
3943
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003944 def _canonical_gerrit_googlesource_host(self, host):
3945 git_host = self._canonical_git_googlesource_host(host)
3946 prefix = git_host.split('.', 1)[0]
3947 return prefix + '-review.' + self._GOOGLESOURCE
3948
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003949 def _get_counterpart_host(self, host):
3950 assert host.endswith(self._GOOGLESOURCE)
3951 git = self._canonical_git_googlesource_host(host)
3952 gerrit = self._canonical_gerrit_googlesource_host(git)
3953 return git if gerrit == host else gerrit
3954
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003955 def has_generic_host(self):
3956 """Returns whether generic .googlesource.com has been configured.
3957
3958 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3959 """
3960 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3961 if host == '.' + self._GOOGLESOURCE:
3962 return True
3963 return False
3964
3965 def _get_git_gerrit_identity_pairs(self):
3966 """Returns map from canonic host to pair of identities (Git, Gerrit).
3967
3968 One of identities might be None, meaning not configured.
3969 """
3970 host_to_identity_pairs = {}
3971 for host, identity, _ in self.get_hosts_with_creds():
3972 canonical = self._canonical_git_googlesource_host(host)
3973 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3974 idx = 0 if canonical == host else 1
3975 pair[idx] = identity
3976 return host_to_identity_pairs
3977
3978 def get_partially_configured_hosts(self):
3979 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003980 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3981 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3982 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003983
3984 def get_conflicting_hosts(self):
3985 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003986 host
3987 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003988 if None not in (i1, i2) and i1 != i2)
3989
3990 def get_duplicated_hosts(self):
3991 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3992 return set(host for host, count in counters.iteritems() if count > 1)
3993
3994 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3995 'chromium.googlesource.com': 'chromium.org',
3996 'chrome-internal.googlesource.com': 'google.com',
3997 }
3998
3999 def get_hosts_with_wrong_identities(self):
4000 """Finds hosts which **likely** reference wrong identities.
4001
4002 Note: skips hosts which have conflicting identities for Git and Gerrit.
4003 """
4004 hosts = set()
4005 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4006 pair = self._get_git_gerrit_identity_pairs().get(host)
4007 if pair and pair[0] == pair[1]:
4008 _, domain = self._parse_identity(pair[0])
4009 if domain != expected:
4010 hosts.add(host)
4011 return hosts
4012
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004013 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004014 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004015 hosts = sorted(hosts)
4016 assert hosts
4017 if extra_column_func is None:
4018 extras = [''] * len(hosts)
4019 else:
4020 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004021 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4022 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004023 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004024 lines.append(tmpl % he)
4025 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004026
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004027 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004028 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004029 yield ('.googlesource.com wildcard record detected',
4030 ['Chrome Infrastructure team recommends to list full host names '
4031 'explicitly.'],
4032 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004033
4034 dups = self.get_duplicated_hosts()
4035 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004036 yield ('The following hosts were defined twice',
4037 self._format_hosts(dups),
4038 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004039
4040 partial = self.get_partially_configured_hosts()
4041 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004042 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4043 'These hosts are missing',
4044 self._format_hosts(partial, lambda host: 'but %s defined' %
4045 self._get_counterpart_host(host)),
4046 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004047
4048 conflicting = self.get_conflicting_hosts()
4049 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004050 yield ('The following Git hosts have differing credentials from their '
4051 'Gerrit counterparts',
4052 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4053 tuple(self._get_git_gerrit_identity_pairs()[host])),
4054 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004055
4056 wrong = self.get_hosts_with_wrong_identities()
4057 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004058 yield ('These hosts likely use wrong identity',
4059 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4060 (self._get_git_gerrit_identity_pairs()[host][0],
4061 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4062 wrong)
4063
4064 def find_and_report_problems(self):
4065 """Returns True if there was at least one problem, else False."""
4066 found = False
4067 bad_hosts = set()
4068 for title, sublines, hosts in self._find_problems():
4069 if not found:
4070 found = True
4071 print('\n\n.gitcookies problem report:\n')
4072 bad_hosts.update(hosts or [])
4073 print(' %s%s' % (title , (':' if sublines else '')))
4074 if sublines:
4075 print()
4076 print(' %s' % '\n '.join(sublines))
4077 print()
4078
4079 if bad_hosts:
4080 assert found
4081 print(' You can manually remove corresponding lines in your %s file and '
4082 'visit the following URLs with correct account to generate '
4083 'correct credential lines:\n' %
4084 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4085 print(' %s' % '\n '.join(sorted(set(
4086 gerrit_util.CookiesAuthenticator().get_new_password_url(
4087 self._canonical_git_googlesource_host(host))
4088 for host in bad_hosts
4089 ))))
4090 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004091
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004092
4093def CMDcreds_check(parser, args):
4094 """Checks credentials and suggests changes."""
4095 _, _ = parser.parse_args(args)
4096
4097 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004098 DieWithError(
4099 'This command is not designed for GCE, are you on a bot?\n'
4100 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004101
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004102 checker = _GitCookiesChecker()
4103 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004104
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004105 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004106 checker.print_current_creds(include_netrc=True)
4107
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004108 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004109 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004110 return 0
4111 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004112
4113
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004114@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004115def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004116 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004117
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004118 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004119 # TODO(tandrii): remove this once we switch to Gerrit.
4120 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004121 parser.add_option('--activate-update', action='store_true',
4122 help='activate auto-updating [rietveld] section in '
4123 '.git/config')
4124 parser.add_option('--deactivate-update', action='store_true',
4125 help='deactivate auto-updating [rietveld] section in '
4126 '.git/config')
4127 options, args = parser.parse_args(args)
4128
4129 if options.deactivate_update:
4130 RunGit(['config', 'rietveld.autoupdate', 'false'])
4131 return
4132
4133 if options.activate_update:
4134 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4135 return
4136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004137 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004138 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139 return 0
4140
4141 url = args[0]
4142 if not url.endswith('codereview.settings'):
4143 url = os.path.join(url, 'codereview.settings')
4144
4145 # Load code review settings and download hooks (if available).
4146 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4147 return 0
4148
4149
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004150def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004151 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004152 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4153 branch = ShortBranchName(branchref)
4154 _, args = parser.parse_args(args)
4155 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004156 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004157 return RunGit(['config', 'branch.%s.base-url' % branch],
4158 error_ok=False).strip()
4159 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004161 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4162 error_ok=False).strip()
4163
4164
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004165def color_for_status(status):
4166 """Maps a Changelist status to color, for CMDstatus and other tools."""
4167 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004168 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004169 'waiting': Fore.BLUE,
4170 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004171 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004172 'lgtm': Fore.GREEN,
4173 'commit': Fore.MAGENTA,
4174 'closed': Fore.CYAN,
4175 'error': Fore.WHITE,
4176 }.get(status, Fore.WHITE)
4177
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004178
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004179def get_cl_statuses(changes, fine_grained, max_processes=None):
4180 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004181
4182 If fine_grained is true, this will fetch CL statuses from the server.
4183 Otherwise, simply indicate if there's a matching url for the given branches.
4184
4185 If max_processes is specified, it is used as the maximum number of processes
4186 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4187 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004188
4189 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004190 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004191 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004192 upload.verbosity = 0
4193
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004194 if not changes:
4195 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004196
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004197 if not fine_grained:
4198 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004199 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004200 for cl in changes:
4201 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004202 return
4203
4204 # First, sort out authentication issues.
4205 logging.debug('ensuring credentials exist')
4206 for cl in changes:
4207 cl.EnsureAuthenticated(force=False, refresh=True)
4208
4209 def fetch(cl):
4210 try:
4211 return (cl, cl.GetStatus())
4212 except:
4213 # See http://crbug.com/629863.
4214 logging.exception('failed to fetch status for %s:', cl)
4215 raise
4216
4217 threads_count = len(changes)
4218 if max_processes:
4219 threads_count = max(1, min(threads_count, max_processes))
4220 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4221
4222 pool = ThreadPool(threads_count)
4223 fetched_cls = set()
4224 try:
4225 it = pool.imap_unordered(fetch, changes).__iter__()
4226 while True:
4227 try:
4228 cl, status = it.next(timeout=5)
4229 except multiprocessing.TimeoutError:
4230 break
4231 fetched_cls.add(cl)
4232 yield cl, status
4233 finally:
4234 pool.close()
4235
4236 # Add any branches that failed to fetch.
4237 for cl in set(changes) - fetched_cls:
4238 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004239
rmistry@google.com2dd99862015-06-22 12:22:18 +00004240
4241def upload_branch_deps(cl, args):
4242 """Uploads CLs of local branches that are dependents of the current branch.
4243
4244 If the local branch dependency tree looks like:
4245 test1 -> test2.1 -> test3.1
4246 -> test3.2
4247 -> test2.2 -> test3.3
4248
4249 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4250 run on the dependent branches in this order:
4251 test2.1, test3.1, test3.2, test2.2, test3.3
4252
4253 Note: This function does not rebase your local dependent branches. Use it when
4254 you make a change to the parent branch that will not conflict with its
4255 dependent branches, and you would like their dependencies updated in
4256 Rietveld.
4257 """
4258 if git_common.is_dirty_git_tree('upload-branch-deps'):
4259 return 1
4260
4261 root_branch = cl.GetBranch()
4262 if root_branch is None:
4263 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4264 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004265 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004266 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4267 'patchset dependencies without an uploaded CL.')
4268
4269 branches = RunGit(['for-each-ref',
4270 '--format=%(refname:short) %(upstream:short)',
4271 'refs/heads'])
4272 if not branches:
4273 print('No local branches found.')
4274 return 0
4275
4276 # Create a dictionary of all local branches to the branches that are dependent
4277 # on it.
4278 tracked_to_dependents = collections.defaultdict(list)
4279 for b in branches.splitlines():
4280 tokens = b.split()
4281 if len(tokens) == 2:
4282 branch_name, tracked = tokens
4283 tracked_to_dependents[tracked].append(branch_name)
4284
vapiera7fbd5a2016-06-16 09:17:49 -07004285 print()
4286 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004287 dependents = []
4288 def traverse_dependents_preorder(branch, padding=''):
4289 dependents_to_process = tracked_to_dependents.get(branch, [])
4290 padding += ' '
4291 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004292 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004293 dependents.append(dependent)
4294 traverse_dependents_preorder(dependent, padding)
4295 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004297
4298 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004300 return 0
4301
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004302 confirm_or_exit('This command will checkout all dependent branches and run '
4303 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004304
andybons@chromium.org962f9462016-02-03 20:00:42 +00004305 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004306 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004307 args.extend(['-t', 'Updated patchset dependency'])
4308
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309 # Record all dependents that failed to upload.
4310 failures = {}
4311 # Go through all dependents, checkout the branch and upload.
4312 try:
4313 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print()
4315 print('--------------------------------------')
4316 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004317 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004318 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004319 try:
4320 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004321 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004322 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004323 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004324 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004325 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004326 finally:
4327 # Swap back to the original root branch.
4328 RunGit(['checkout', '-q', root_branch])
4329
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print()
4331 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004332 for dependent_branch in dependents:
4333 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004334 print(' %s : %s' % (dependent_branch, upload_status))
4335 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004336
4337 return 0
4338
4339
kmarshall3bff56b2016-06-06 18:31:47 -07004340def CMDarchive(parser, args):
4341 """Archives and deletes branches associated with closed changelists."""
4342 parser.add_option(
4343 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004344 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004345 parser.add_option(
4346 '-f', '--force', action='store_true',
4347 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004348 parser.add_option(
4349 '-d', '--dry-run', action='store_true',
4350 help='Skip the branch tagging and removal steps.')
4351 parser.add_option(
4352 '-t', '--notags', action='store_true',
4353 help='Do not tag archived branches. '
4354 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004355
4356 auth.add_auth_options(parser)
4357 options, args = parser.parse_args(args)
4358 if args:
4359 parser.error('Unsupported args: %s' % ' '.join(args))
4360 auth_config = auth.extract_auth_config_from_options(options)
4361
4362 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4363 if not branches:
4364 return 0
4365
vapiera7fbd5a2016-06-16 09:17:49 -07004366 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004367 changes = [Changelist(branchref=b, auth_config=auth_config)
4368 for b in branches.splitlines()]
4369 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4370 statuses = get_cl_statuses(changes,
4371 fine_grained=True,
4372 max_processes=options.maxjobs)
4373 proposal = [(cl.GetBranch(),
4374 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4375 for cl, status in statuses
4376 if status == 'closed']
4377 proposal.sort()
4378
4379 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004380 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004381 return 0
4382
4383 current_branch = GetCurrentBranch()
4384
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004386 if options.notags:
4387 for next_item in proposal:
4388 print(' ' + next_item[0])
4389 else:
4390 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4391 for next_item in proposal:
4392 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004393
kmarshall9249e012016-08-23 12:02:16 -07004394 # Quit now on precondition failure or if instructed by the user, either
4395 # via an interactive prompt or by command line flags.
4396 if options.dry_run:
4397 print('\nNo changes were made (dry run).\n')
4398 return 0
4399 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004400 print('You are currently on a branch \'%s\' which is associated with a '
4401 'closed codereview issue, so archive cannot proceed. Please '
4402 'checkout another branch and run this command again.' %
4403 current_branch)
4404 return 1
kmarshall9249e012016-08-23 12:02:16 -07004405 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004406 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4407 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004408 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004409 return 1
4410
4411 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004412 if not options.notags:
4413 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004414 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004415
vapiera7fbd5a2016-06-16 09:17:49 -07004416 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004417
4418 return 0
4419
4420
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004421def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004422 """Show status of changelists.
4423
4424 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004425 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004426 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004427 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004428 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004429 - Magenta in the commit queue
4430 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004431 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004432
4433 Also see 'git cl comments'.
4434 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004436 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004437 parser.add_option('-f', '--fast', action='store_true',
4438 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004439 parser.add_option(
4440 '-j', '--maxjobs', action='store', type=int,
4441 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004442
4443 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004444 _add_codereview_issue_select_options(
4445 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004446 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004447 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004448 if args:
4449 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004450 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451
iannuccie53c9352016-08-17 14:40:40 -07004452 if options.issue is not None and not options.field:
4453 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004455 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004456 cl = Changelist(auth_config=auth_config, issue=options.issue,
4457 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004458 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004460 elif options.field == 'id':
4461 issueid = cl.GetIssue()
4462 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004465 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004467 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004468 elif options.field == 'status':
4469 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004470 elif options.field == 'url':
4471 url = cl.GetIssueURL()
4472 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004473 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004474 return 0
4475
4476 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4477 if not branches:
4478 print('No local branch found.')
4479 return 0
4480
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004481 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004482 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004483 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004484 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004485 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004486 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004487 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004488
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004489 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004490 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4491 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4492 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004493 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004494 c, status = output.next()
4495 branch_statuses[c.GetBranch()] = status
4496 status = branch_statuses.pop(branch)
4497 url = cl.GetIssueURL()
4498 if url and (not status or status == 'error'):
4499 # The issue probably doesn't exist anymore.
4500 url += ' (broken)'
4501
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004502 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004503 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004504 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004505 color = ''
4506 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004507 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004508 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004509 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004510 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004511
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004512
4513 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004514 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004515 print('Current branch: %s' % branch)
4516 for cl in changes:
4517 if cl.GetBranch() == branch:
4518 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004519 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004520 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004521 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004522 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004523 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004524 print('Issue description:')
4525 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004526 return 0
4527
4528
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004529def colorize_CMDstatus_doc():
4530 """To be called once in main() to add colors to git cl status help."""
4531 colors = [i for i in dir(Fore) if i[0].isupper()]
4532
4533 def colorize_line(line):
4534 for color in colors:
4535 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004536 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004537 indent = len(line) - len(line.lstrip(' ')) + 1
4538 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4539 return line
4540
4541 lines = CMDstatus.__doc__.splitlines()
4542 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4543
4544
phajdan.jre328cf92016-08-22 04:12:17 -07004545def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004546 if path == '-':
4547 json.dump(contents, sys.stdout)
4548 else:
4549 with open(path, 'w') as f:
4550 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004551
4552
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004553@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004554def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004555 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556
4557 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004558 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004559 parser.add_option('-r', '--reverse', action='store_true',
4560 help='Lookup the branch(es) for the specified issues. If '
4561 'no issues are specified, all branches with mapped '
4562 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004563 parser.add_option('--json',
4564 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004565 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004566 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004567 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004568
dnj@chromium.org406c4402015-03-03 17:22:28 +00004569 if options.reverse:
4570 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004571 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004572 # Reverse issue lookup.
4573 issue_branch_map = {}
4574 for branch in branches:
4575 cl = Changelist(branchref=branch)
4576 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4577 if not args:
4578 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004579 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004580 for issue in args:
4581 if not issue:
4582 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004583 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004584 print('Branch for issue number %s: %s' % (
4585 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004586 if options.json:
4587 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004588 return 0
4589
4590 if len(args) > 0:
4591 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4592 if not issue.valid:
4593 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4594 'or no argument to list it.\n'
4595 'Maybe you want to run git cl status?')
4596 cl = Changelist(codereview=issue.codereview)
4597 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004598 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004599 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004600 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4601 if options.json:
4602 write_json(options.json, {
4603 'issue': cl.GetIssue(),
4604 'issue_url': cl.GetIssueURL(),
4605 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 return 0
4607
4608
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004609def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004610 """Shows or posts review comments for any changelist."""
4611 parser.add_option('-a', '--add-comment', dest='comment',
4612 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004613 parser.add_option('-i', '--issue', dest='issue',
4614 help='review issue id (defaults to current issue). '
4615 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004616 parser.add_option('-m', '--machine-readable', dest='readable',
4617 action='store_false', default=True,
4618 help='output comments in a format compatible with '
4619 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004620 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004621 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004622 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004623 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004624 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004625 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004626 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004627
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004628 issue = None
4629 if options.issue:
4630 try:
4631 issue = int(options.issue)
4632 except ValueError:
4633 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004634 if not options.forced_codereview:
4635 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004636
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004637 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004638 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004639 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004640
4641 if options.comment:
4642 cl.AddComment(options.comment)
4643 return 0
4644
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004645 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4646 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004647 for comment in summary:
4648 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004649 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004650 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004651 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004652 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004653 color = Fore.MAGENTA
4654 else:
4655 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004656 print('\n%s%s %s%s\n%s' % (
4657 color,
4658 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4659 comment.sender,
4660 Fore.RESET,
4661 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4662
smut@google.comc85ac942015-09-15 16:34:43 +00004663 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004664 def pre_serialize(c):
4665 dct = c.__dict__.copy()
4666 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4667 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004668 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004669 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004670 return 0
4671
4672
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004673@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004674def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004675 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004676 parser.add_option('-d', '--display', action='store_true',
4677 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004678 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004679 help='New description to set for this issue (- for stdin, '
4680 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004681 parser.add_option('-f', '--force', action='store_true',
4682 help='Delete any unpublished Gerrit edits for this issue '
4683 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004684
4685 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004686 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004687 options, args = parser.parse_args(args)
4688 _process_codereview_select_options(parser, options)
4689
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004690 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004691 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004692 target_issue_arg = ParseIssueNumberArgument(args[0],
4693 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004694 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004695 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004696
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004697 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004698
martiniss6eda05f2016-06-30 10:18:35 -07004699 kwargs = {
4700 'auth_config': auth_config,
4701 'codereview': options.forced_codereview,
4702 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004703 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004704 if target_issue_arg:
4705 kwargs['issue'] = target_issue_arg.issue
4706 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004707 if target_issue_arg.codereview and not options.forced_codereview:
4708 detected_codereview_from_url = True
4709 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004710
4711 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004712 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004713 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004714 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004715
4716 if detected_codereview_from_url:
4717 logging.info('canonical issue/change URL: %s (type: %s)\n',
4718 cl.GetIssueURL(), target_issue_arg.codereview)
4719
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004720 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004721
smut@google.com34fb6b12015-07-13 20:03:26 +00004722 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004723 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004724 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004725
4726 if options.new_description:
4727 text = options.new_description
4728 if text == '-':
4729 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004730 elif text == '+':
4731 base_branch = cl.GetCommonAncestorWithUpstream()
4732 change = cl.GetChange(base_branch, None, local_description=True)
4733 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004734
4735 description.set_description(text)
4736 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004737 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004738
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004739 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004740 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004741 return 0
4742
4743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004744def CreateDescriptionFromLog(args):
4745 """Pulls out the commit log to use as a base for the CL description."""
4746 log_args = []
4747 if len(args) == 1 and not args[0].endswith('.'):
4748 log_args = [args[0] + '..']
4749 elif len(args) == 1 and args[0].endswith('...'):
4750 log_args = [args[0][:-1]]
4751 elif len(args) == 2:
4752 log_args = [args[0] + '..' + args[1]]
4753 else:
4754 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004755 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004756
4757
thestig@chromium.org44202a22014-03-11 19:22:18 +00004758def CMDlint(parser, args):
4759 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004760 parser.add_option('--filter', action='append', metavar='-x,+y',
4761 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004762 auth.add_auth_options(parser)
4763 options, args = parser.parse_args(args)
4764 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004765
4766 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004767 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004768 try:
4769 import cpplint
4770 import cpplint_chromium
4771 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004772 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004773 return 1
4774
4775 # Change the current working directory before calling lint so that it
4776 # shows the correct base.
4777 previous_cwd = os.getcwd()
4778 os.chdir(settings.GetRoot())
4779 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004780 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004781 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4782 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004783 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004784 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004785 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004786
4787 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004788 command = args + files
4789 if options.filter:
4790 command = ['--filter=' + ','.join(options.filter)] + command
4791 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004792
4793 white_regex = re.compile(settings.GetLintRegex())
4794 black_regex = re.compile(settings.GetLintIgnoreRegex())
4795 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4796 for filename in filenames:
4797 if white_regex.match(filename):
4798 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004799 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004800 else:
4801 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4802 extra_check_functions)
4803 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004804 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004805 finally:
4806 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004807 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004808 if cpplint._cpplint_state.error_count != 0:
4809 return 1
4810 return 0
4811
4812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004813def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004814 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004815 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004816 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004817 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004818 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004819 parser.add_option('--all', action='store_true',
4820 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004821 auth.add_auth_options(parser)
4822 options, args = parser.parse_args(args)
4823 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004824
sbc@chromium.org71437c02015-04-09 19:29:40 +00004825 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004826 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004827 return 1
4828
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004829 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004830 if args:
4831 base_branch = args[0]
4832 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004833 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004834 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004835
Aaron Gable8076c282017-11-29 14:39:41 -08004836 if options.all:
4837 base_change = cl.GetChange(base_branch, None)
4838 files = [('M', f) for f in base_change.AllFiles()]
4839 change = presubmit_support.GitChange(
4840 base_change.Name(),
4841 base_change.FullDescriptionText(),
4842 base_change.RepositoryRoot(),
4843 files,
4844 base_change.issue,
4845 base_change.patchset,
4846 base_change.author_email,
4847 base_change._upstream)
4848 else:
4849 change = cl.GetChange(base_branch, None)
4850
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004851 cl.RunHook(
4852 committing=not options.upload,
4853 may_prompt=False,
4854 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004855 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004856 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004857
4858
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004859def GenerateGerritChangeId(message):
4860 """Returns Ixxxxxx...xxx change id.
4861
4862 Works the same way as
4863 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4864 but can be called on demand on all platforms.
4865
4866 The basic idea is to generate git hash of a state of the tree, original commit
4867 message, author/committer info and timestamps.
4868 """
4869 lines = []
4870 tree_hash = RunGitSilent(['write-tree'])
4871 lines.append('tree %s' % tree_hash.strip())
4872 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4873 if code == 0:
4874 lines.append('parent %s' % parent.strip())
4875 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4876 lines.append('author %s' % author.strip())
4877 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4878 lines.append('committer %s' % committer.strip())
4879 lines.append('')
4880 # Note: Gerrit's commit-hook actually cleans message of some lines and
4881 # whitespace. This code is not doing this, but it clearly won't decrease
4882 # entropy.
4883 lines.append(message)
4884 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4885 stdin='\n'.join(lines))
4886 return 'I%s' % change_hash.strip()
4887
4888
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004889def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004890 """Computes the remote branch ref to use for the CL.
4891
4892 Args:
4893 remote (str): The git remote for the CL.
4894 remote_branch (str): The git remote branch for the CL.
4895 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004896 """
4897 if not (remote and remote_branch):
4898 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004899
wittman@chromium.org455dc922015-01-26 20:15:50 +00004900 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004901 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004902 # refs, which are then translated into the remote full symbolic refs
4903 # below.
4904 if '/' not in target_branch:
4905 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4906 else:
4907 prefix_replacements = (
4908 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4909 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4910 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4911 )
4912 match = None
4913 for regex, replacement in prefix_replacements:
4914 match = re.search(regex, target_branch)
4915 if match:
4916 remote_branch = target_branch.replace(match.group(0), replacement)
4917 break
4918 if not match:
4919 # This is a branch path but not one we recognize; use as-is.
4920 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004921 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4922 # Handle the refs that need to land in different refs.
4923 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004924
wittman@chromium.org455dc922015-01-26 20:15:50 +00004925 # Create the true path to the remote branch.
4926 # Does the following translation:
4927 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4928 # * refs/remotes/origin/master -> refs/heads/master
4929 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4930 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4931 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4932 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4933 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4934 'refs/heads/')
4935 elif remote_branch.startswith('refs/remotes/branch-heads'):
4936 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004937
wittman@chromium.org455dc922015-01-26 20:15:50 +00004938 return remote_branch
4939
4940
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004941def cleanup_list(l):
4942 """Fixes a list so that comma separated items are put as individual items.
4943
4944 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4945 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4946 """
4947 items = sum((i.split(',') for i in l), [])
4948 stripped_items = (i.strip() for i in items)
4949 return sorted(filter(None, stripped_items))
4950
4951
Aaron Gable4db38df2017-11-03 14:59:07 -07004952@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004953def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004954 """Uploads the current changelist to codereview.
4955
4956 Can skip dependency patchset uploads for a branch by running:
4957 git config branch.branch_name.skip-deps-uploads True
4958 To unset run:
4959 git config --unset branch.branch_name.skip-deps-uploads
4960 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004961
4962 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4963 a bug number, this bug number is automatically populated in the CL
4964 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004965
4966 If subject contains text in square brackets or has "<text>: " prefix, such
4967 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4968 [git-cl] add support for hashtags
4969 Foo bar: implement foo
4970 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004971 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004972 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4973 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004974 parser.add_option('--bypass-watchlists', action='store_true',
4975 dest='bypass_watchlists',
4976 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004977 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004978 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004979 parser.add_option('--message', '-m', dest='message',
4980 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004981 parser.add_option('-b', '--bug',
4982 help='pre-populate the bug number(s) for this issue. '
4983 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004984 parser.add_option('--message-file', dest='message_file',
4985 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004986 parser.add_option('--title', '-t', dest='title',
4987 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004988 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004989 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004990 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004991 parser.add_option('--tbrs',
4992 action='append', default=[],
4993 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004994 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004995 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004996 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004997 parser.add_option('--hashtag', dest='hashtags',
4998 action='append', default=[],
4999 help=('Gerrit hashtag for new CL; '
5000 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005001 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005002 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005003 parser.add_option('--emulate_svn_auto_props',
5004 '--emulate-svn-auto-props',
5005 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005006 dest="emulate_svn_auto_props",
5007 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005008 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005009 help='tell the commit queue to commit this patchset; '
5010 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005011 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005012 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005013 metavar='TARGET',
5014 help='Apply CL to remote ref TARGET. ' +
5015 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005016 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005017 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005018 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005019 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005020 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005021 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005022 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5023 const='TBR', help='add a set of OWNERS to TBR')
5024 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5025 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005026 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5027 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005028 help='Send the patchset to do a CQ dry run right after '
5029 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005030 parser.add_option('--dependencies', action='store_true',
5031 help='Uploads CLs of all the local branches that depend on '
5032 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005033 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5034 help='Sends your change to the CQ after an approval. Only '
5035 'works on repos that have the Auto-Submit label '
5036 'enabled')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005037
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005038 # TODO: remove Rietveld flags
5039 parser.add_option('--private', action='store_true',
5040 help='set the review private (rietveld only)')
5041 parser.add_option('--email', default=None,
5042 help='email address to use to connect to Rietveld')
5043
rmistry@google.com2dd99862015-06-22 12:22:18 +00005044 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005045 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005046 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005047 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005048 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005049 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005050
sbc@chromium.org71437c02015-04-09 19:29:40 +00005051 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005052 return 1
5053
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005054 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005055 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005056 options.cc = cleanup_list(options.cc)
5057
tandriib80458a2016-06-23 12:20:07 -07005058 if options.message_file:
5059 if options.message:
5060 parser.error('only one of --message and --message-file allowed.')
5061 options.message = gclient_utils.FileRead(options.message_file)
5062 options.message_file = None
5063
tandrii4d0545a2016-07-06 03:56:49 -07005064 if options.cq_dry_run and options.use_commit_queue:
5065 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5066
Aaron Gableedbc4132017-09-11 13:22:28 -07005067 if options.use_commit_queue:
5068 options.send_mail = True
5069
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005070 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5071 settings.GetIsGerrit()
5072
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005073 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005074 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005075
5076
Francois Dorayd42c6812017-05-30 15:10:20 -04005077@subcommand.usage('--description=<description file>')
5078def CMDsplit(parser, args):
5079 """Splits a branch into smaller branches and uploads CLs.
5080
5081 Creates a branch and uploads a CL for each group of files modified in the
5082 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005083 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005084 the shared OWNERS file.
5085 """
5086 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005087 help="A text file containing a CL description in which "
5088 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005089 parser.add_option("-c", "--comment", dest="comment_file",
5090 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005091 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5092 default=False,
5093 help="List the files and reviewers for each CL that would "
5094 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005095 options, _ = parser.parse_args(args)
5096
5097 if not options.description_file:
5098 parser.error('No --description flag specified.')
5099
5100 def WrappedCMDupload(args):
5101 return CMDupload(OptionParser(), args)
5102
5103 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005104 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005105
5106
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005107@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005108def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005109 """DEPRECATED: Used to commit the current changelist via git-svn."""
5110 message = ('git-cl no longer supports committing to SVN repositories via '
5111 'git-svn. You probably want to use `git cl land` instead.')
5112 print(message)
5113 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005114
5115
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005116# Two special branches used by git cl land.
5117MERGE_BRANCH = 'git-cl-commit'
5118CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5119
5120
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005121@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005122def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005123 """Commits the current changelist via git.
5124
5125 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5126 upstream and closes the issue automatically and atomically.
5127
5128 Otherwise (in case of Rietveld):
5129 Squashes branch into a single commit.
5130 Updates commit message with metadata (e.g. pointer to review).
5131 Pushes the code upstream.
5132 Updates review and closes.
5133 """
5134 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5135 help='bypass upload presubmit hook')
5136 parser.add_option('-m', dest='message',
5137 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005138 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005139 help="force yes to questions (don't prompt)")
5140 parser.add_option('-c', dest='contributor',
5141 help="external contributor for patch (appended to " +
5142 "description and used as author for git). Should be " +
5143 "formatted as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005144 auth.add_auth_options(parser)
5145 (options, args) = parser.parse_args(args)
5146 auth_config = auth.extract_auth_config_from_options(options)
5147
5148 cl = Changelist(auth_config=auth_config)
5149
Robert Iannucci2e73d432018-03-14 01:10:47 -07005150 if not cl.IsGerrit():
5151 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005152
Robert Iannucci2e73d432018-03-14 01:10:47 -07005153 if options.message:
5154 # This could be implemented, but it requires sending a new patch to
5155 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5156 # Besides, Gerrit has the ability to change the commit message on submit
5157 # automatically, thus there is no need to support this option (so far?).
5158 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005159 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005160 parser.error(
5161 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5162 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5163 'the contributor\'s "name <email>". If you can\'t upload such a '
5164 'commit for review, contact your repository admin and request'
5165 '"Forge-Author" permission.')
5166 if not cl.GetIssue():
5167 DieWithError('You must upload the change first to Gerrit.\n'
5168 ' If you would rather have `git cl land` upload '
5169 'automatically for you, see http://crbug.com/642759')
5170 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5171 options.verbose)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005172
5173
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005174def PushToGitWithAutoRebase(remote, branch, original_description,
5175 git_numberer_enabled, max_attempts=3):
5176 """Pushes current HEAD commit on top of remote's branch.
5177
5178 Attempts to fetch and autorebase on push failures.
5179 Adds git number footers on the fly.
5180
5181 Returns integer code from last command.
5182 """
5183 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5184 code = 0
5185 attempts_left = max_attempts
5186 while attempts_left:
5187 attempts_left -= 1
5188 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5189
5190 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5191 # If fetch fails, retry.
5192 print('Fetching %s/%s...' % (remote, branch))
5193 code, out = RunGitWithCode(
5194 ['retry', 'fetch', remote,
5195 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5196 if code:
5197 print('Fetch failed with exit code %d.' % code)
5198 print(out.strip())
5199 continue
5200
5201 print('Cherry-picking commit on top of latest %s' % branch)
5202 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5203 suppress_stderr=True)
5204 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5205 code, out = RunGitWithCode(['cherry-pick', cherry])
5206 if code:
5207 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5208 'the following files have merge conflicts:' %
5209 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005210 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5211 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005212 print('Please rebase your patch and try again.')
5213 RunGitWithCode(['cherry-pick', '--abort'])
5214 break
5215
5216 commit_desc = ChangeDescription(original_description)
5217 if git_numberer_enabled:
5218 logging.debug('Adding git number footers')
5219 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5220 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5221 branch)
5222 # Ensure timestamps are monotonically increasing.
5223 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5224 _get_committer_timestamp('HEAD'))
5225 _git_amend_head(commit_desc.description, timestamp)
5226
5227 code, out = RunGitWithCode(
5228 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5229 print(out)
5230 if code == 0:
5231 break
5232 if IsFatalPushFailure(out):
5233 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005234 'user.email are correct and you have push access to the repo.\n'
5235 'Hint: run command below to diangose common Git/Gerrit credential '
5236 'problems:\n'
5237 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005238 break
5239 return code
5240
5241
5242def IsFatalPushFailure(push_stdout):
5243 """True if retrying push won't help."""
5244 return '(prohibited by Gerrit)' in push_stdout
5245
5246
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005247@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005248def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005249 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005250 parser.add_option('-b', dest='newbranch',
5251 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005252 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005253 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005254 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005255 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005256 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005257 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005258 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005259 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005260 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005261 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005262
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005263
5264 group = optparse.OptionGroup(
5265 parser,
5266 'Options for continuing work on the current issue uploaded from a '
5267 'different clone (e.g. different machine). Must be used independently '
5268 'from the other options. No issue number should be specified, and the '
5269 'branch must have an issue number associated with it')
5270 group.add_option('--reapply', action='store_true', dest='reapply',
5271 help='Reset the branch and reapply the issue.\n'
5272 'CAUTION: This will undo any local changes in this '
5273 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005274
5275 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005276 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005277 parser.add_option_group(group)
5278
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005279 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005280 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005281 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005282 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005283 auth_config = auth.extract_auth_config_from_options(options)
5284
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005285 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005286 if options.newbranch:
5287 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005288 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005289 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005290
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005291 cl = Changelist(auth_config=auth_config,
5292 codereview=options.forced_codereview)
5293 if not cl.GetIssue():
5294 parser.error('current branch must have an associated issue')
5295
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005296 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005297 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005298 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005299
5300 RunGit(['reset', '--hard', upstream])
5301 if options.pull:
5302 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005303
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005304 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5305 options.directory)
5306
5307 if len(args) != 1 or not args[0]:
5308 parser.error('Must specify issue number or url')
5309
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005310 target_issue_arg = ParseIssueNumberArgument(args[0],
5311 options.forced_codereview)
5312 if not target_issue_arg.valid:
5313 parser.error('invalid codereview url or CL id')
5314
5315 cl_kwargs = {
5316 'auth_config': auth_config,
5317 'codereview_host': target_issue_arg.hostname,
5318 'codereview': options.forced_codereview,
5319 }
5320 detected_codereview_from_url = False
5321 if target_issue_arg.codereview and not options.forced_codereview:
5322 detected_codereview_from_url = True
5323 cl_kwargs['codereview'] = target_issue_arg.codereview
5324 cl_kwargs['issue'] = target_issue_arg.issue
5325
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005326 # We don't want uncommitted changes mixed up with the patch.
5327 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005328 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005329
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005330 if options.newbranch:
5331 if options.force:
5332 RunGit(['branch', '-D', options.newbranch],
5333 stderr=subprocess2.PIPE, error_ok=True)
5334 RunGit(['new-branch', options.newbranch])
5335
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005336 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005337
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005338 if cl.IsGerrit():
5339 if options.reject:
5340 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005341 if options.directory:
5342 parser.error('--directory is not supported with Gerrit codereview.')
5343
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005344 if detected_codereview_from_url:
5345 print('canonical issue/change URL: %s (type: %s)\n' %
5346 (cl.GetIssueURL(), target_issue_arg.codereview))
5347
5348 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005349 options.nocommit, options.directory,
5350 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005351
5352
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005353def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005354 """Fetches the tree status and returns either 'open', 'closed',
5355 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005356 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005357 if url:
5358 status = urllib2.urlopen(url).read().lower()
5359 if status.find('closed') != -1 or status == '0':
5360 return 'closed'
5361 elif status.find('open') != -1 or status == '1':
5362 return 'open'
5363 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005364 return 'unset'
5365
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005366
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005367def GetTreeStatusReason():
5368 """Fetches the tree status from a json url and returns the message
5369 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005370 url = settings.GetTreeStatusUrl()
5371 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005372 connection = urllib2.urlopen(json_url)
5373 status = json.loads(connection.read())
5374 connection.close()
5375 return status['message']
5376
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005378def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005379 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005380 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005381 status = GetTreeStatus()
5382 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005383 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005384 return 2
5385
vapiera7fbd5a2016-06-16 09:17:49 -07005386 print('The tree is %s' % status)
5387 print()
5388 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005389 if status != 'open':
5390 return 1
5391 return 0
5392
5393
maruel@chromium.org15192402012-09-06 12:38:29 +00005394def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005395 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005396 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005397 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005398 '-b', '--bot', action='append',
5399 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5400 'times to specify multiple builders. ex: '
5401 '"-b win_rel -b win_layout". See '
5402 'the try server waterfall for the builders name and the tests '
5403 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005404 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005405 '-B', '--bucket', default='',
5406 help=('Buildbucket bucket to send the try requests.'))
5407 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005408 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005409 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005410 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005411 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005412 help='Revision to use for the try job; default: the revision will '
5413 'be determined by the try recipe that builder runs, which usually '
5414 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005415 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005416 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005417 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005418 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005419 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005420 '--category', default='git_cl_try', help='Specify custom build category.')
5421 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005422 '--project',
5423 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005424 'in recipe to determine to which repository or directory to '
5425 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005426 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005427 '-p', '--property', dest='properties', action='append', default=[],
5428 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005429 'key2=value2 etc. The value will be treated as '
5430 'json if decodable, or as string otherwise. '
5431 'NOTE: using this may make your try job not usable for CQ, '
5432 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005433 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005434 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5435 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005436 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005437 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005438 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005439 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005440 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005441 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005442
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005443 if options.master and options.master.startswith('luci.'):
5444 parser.error(
5445 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005446 # Make sure that all properties are prop=value pairs.
5447 bad_params = [x for x in options.properties if '=' not in x]
5448 if bad_params:
5449 parser.error('Got properties with missing "=": %s' % bad_params)
5450
maruel@chromium.org15192402012-09-06 12:38:29 +00005451 if args:
5452 parser.error('Unknown arguments: %s' % args)
5453
Koji Ishii31c14782018-01-08 17:17:33 +09005454 cl = Changelist(auth_config=auth_config, issue=options.issue,
5455 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005456 if not cl.GetIssue():
5457 parser.error('Need to upload first')
5458
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005459 if cl.IsGerrit():
5460 # HACK: warm up Gerrit change detail cache to save on RPCs.
5461 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5462
tandriie113dfd2016-10-11 10:20:12 -07005463 error_message = cl.CannotTriggerTryJobReason()
5464 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005465 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005466
borenet6c0efe62016-10-19 08:13:29 -07005467 if options.bucket and options.master:
5468 parser.error('Only one of --bucket and --master may be used.')
5469
qyearsley1fdfcb62016-10-24 13:22:03 -07005470 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005471
qyearsleydd49f942016-10-28 11:57:22 -07005472 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5473 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005474 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005475 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005476 print('git cl try with no bots now defaults to CQ dry run.')
5477 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5478 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005479
borenet6c0efe62016-10-19 08:13:29 -07005480 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005481 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005482 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005483 'of bot requires an initial job from a parent (usually a builder). '
5484 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005485 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005486 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005487
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005488 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005489 # TODO(tandrii): Checking local patchset against remote patchset is only
5490 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5491 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005492 print('Warning: Codereview server has newer patchsets (%s) than most '
5493 'recent upload from local checkout (%s). Did a previous upload '
5494 'fail?\n'
5495 'By default, git cl try uses the latest patchset from '
5496 'codereview, continuing to use patchset %s.\n' %
5497 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005498
tandrii568043b2016-10-11 07:49:18 -07005499 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005500 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005501 except BuildbucketResponseException as ex:
5502 print('ERROR: %s' % ex)
5503 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005504 return 0
5505
5506
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005507def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005508 """Prints info about try jobs associated with current CL."""
5509 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005510 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005511 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005512 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005513 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005514 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005515 '--color', action='store_true', default=setup_color.IS_TTY,
5516 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005517 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005518 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5519 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005520 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005521 '--json', help=('Path of JSON output file to write try job results to,'
5522 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005523 parser.add_option_group(group)
5524 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005525 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005526 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005527 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005528 if args:
5529 parser.error('Unrecognized args: %s' % ' '.join(args))
5530
5531 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005532 cl = Changelist(
5533 issue=options.issue, codereview=options.forced_codereview,
5534 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005535 if not cl.GetIssue():
5536 parser.error('Need to upload first')
5537
tandrii221ab252016-10-06 08:12:04 -07005538 patchset = options.patchset
5539 if not patchset:
5540 patchset = cl.GetMostRecentPatchset()
5541 if not patchset:
5542 parser.error('Codereview doesn\'t know about issue %s. '
5543 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005544 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005545 cl.GetIssue())
5546
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005547 # TODO(tandrii): Checking local patchset against remote patchset is only
5548 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5549 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005550 print('Warning: Codereview server has newer patchsets (%s) than most '
5551 'recent upload from local checkout (%s). Did a previous upload '
5552 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005553 'By default, git cl try-results uses the latest patchset from '
5554 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005555 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005556 try:
tandrii221ab252016-10-06 08:12:04 -07005557 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005558 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005559 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005560 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005561 if options.json:
5562 write_try_results_json(options.json, jobs)
5563 else:
5564 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005565 return 0
5566
5567
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005568@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005569def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005570 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005571 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005572 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005573 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005575 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005576 if args:
5577 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005578 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005579 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005580 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005581 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005582
5583 # Clear configured merge-base, if there is one.
5584 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005585 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005586 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005587 return 0
5588
5589
thestig@chromium.org00858c82013-12-02 23:08:03 +00005590def CMDweb(parser, args):
5591 """Opens the current CL in the web browser."""
5592 _, args = parser.parse_args(args)
5593 if args:
5594 parser.error('Unrecognized args: %s' % ' '.join(args))
5595
5596 issue_url = Changelist().GetIssueURL()
5597 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005598 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005599 return 1
5600
5601 webbrowser.open(issue_url)
5602 return 0
5603
5604
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005605def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005606 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005607 parser.add_option('-d', '--dry-run', action='store_true',
5608 help='trigger in dry run mode')
5609 parser.add_option('-c', '--clear', action='store_true',
5610 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005611 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005612 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005613 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005614 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005615 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005616 if args:
5617 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005618 if options.dry_run and options.clear:
5619 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5620
iannuccie53c9352016-08-17 14:40:40 -07005621 cl = Changelist(auth_config=auth_config, issue=options.issue,
5622 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005623 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005624 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005625 elif options.dry_run:
5626 state = _CQState.DRY_RUN
5627 else:
5628 state = _CQState.COMMIT
5629 if not cl.GetIssue():
5630 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005631 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005632 return 0
5633
5634
groby@chromium.org411034a2013-02-26 15:12:01 +00005635def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005636 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005637 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005638 auth.add_auth_options(parser)
5639 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005640 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005641 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005642 if args:
5643 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005644 cl = Changelist(auth_config=auth_config, issue=options.issue,
5645 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005646 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005647 if not cl.GetIssue():
5648 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005649 cl.CloseIssue()
5650 return 0
5651
5652
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005653def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005654 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005655 parser.add_option(
5656 '--stat',
5657 action='store_true',
5658 dest='stat',
5659 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005660 auth.add_auth_options(parser)
5661 options, args = parser.parse_args(args)
5662 auth_config = auth.extract_auth_config_from_options(options)
5663 if args:
5664 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005665
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005666 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005667 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005668 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005669 if not issue:
5670 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005671
Aaron Gablea718c3e2017-08-28 17:47:28 -07005672 base = cl._GitGetBranchConfigValue('last-upload-hash')
5673 if not base:
5674 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5675 if not base:
5676 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5677 revision_info = detail['revisions'][detail['current_revision']]
5678 fetch_info = revision_info['fetch']['http']
5679 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5680 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005681
Aaron Gablea718c3e2017-08-28 17:47:28 -07005682 cmd = ['git', 'diff']
5683 if options.stat:
5684 cmd.append('--stat')
5685 cmd.append(base)
5686 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005687
5688 return 0
5689
5690
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005691def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005692 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005693 parser.add_option(
5694 '--no-color',
5695 action='store_true',
5696 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005697 parser.add_option(
5698 '--batch',
5699 action='store_true',
5700 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005701 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005702 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005703 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005704
5705 author = RunGit(['config', 'user.email']).strip() or None
5706
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005707 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005708
5709 if args:
5710 if len(args) > 1:
5711 parser.error('Unknown args')
5712 base_branch = args[0]
5713 else:
5714 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005715 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005716
5717 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005718 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5719
5720 if options.batch:
5721 db = owners.Database(change.RepositoryRoot(), file, os.path)
5722 print('\n'.join(db.reviewers_for(affected_files, author)))
5723 return 0
5724
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005725 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005726 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005727 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005728 author,
5729 cl.GetReviewers(),
5730 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005731 disable_color=options.no_color,
5732 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005733
5734
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005735def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005736 """Generates a diff command."""
5737 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005738 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5739 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005740 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005741
5742 if args:
5743 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005744 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005745 diff_cmd.append(arg)
5746 else:
5747 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005748
5749 return diff_cmd
5750
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005751
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005752def MatchingFileType(file_name, extensions):
5753 """Returns true if the file name ends with one of the given extensions."""
5754 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005755
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005756
enne@chromium.org555cfe42014-01-29 18:21:39 +00005757@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005758def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005759 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005760 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005761 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005762 parser.add_option('--full', action='store_true',
5763 help='Reformat the full content of all touched files')
5764 parser.add_option('--dry-run', action='store_true',
5765 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005766 parser.add_option('--python', action='store_true',
5767 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005768 parser.add_option('--js', action='store_true',
5769 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005770 parser.add_option('--diff', action='store_true',
5771 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005772 parser.add_option('--presubmit', action='store_true',
5773 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005774 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005775
Daniel Chengc55eecf2016-12-30 03:11:02 -08005776 # Normalize any remaining args against the current path, so paths relative to
5777 # the current directory are still resolved as expected.
5778 args = [os.path.join(os.getcwd(), arg) for arg in args]
5779
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005780 # git diff generates paths against the root of the repository. Change
5781 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005782 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005783 if rel_base_path:
5784 os.chdir(rel_base_path)
5785
digit@chromium.org29e47272013-05-17 17:01:46 +00005786 # Grab the merge-base commit, i.e. the upstream commit of the current
5787 # branch when it was created or the last time it was rebased. This is
5788 # to cover the case where the user may have called "git fetch origin",
5789 # moving the origin branch to a newer commit, but hasn't rebased yet.
5790 upstream_commit = None
5791 cl = Changelist()
5792 upstream_branch = cl.GetUpstreamBranch()
5793 if upstream_branch:
5794 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5795 upstream_commit = upstream_commit.strip()
5796
5797 if not upstream_commit:
5798 DieWithError('Could not find base commit for this branch. '
5799 'Are you in detached state?')
5800
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005801 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5802 diff_output = RunGit(changed_files_cmd)
5803 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005804 # Filter out files deleted by this CL
5805 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005806
Christopher Lamc5ba6922017-01-24 11:19:14 +11005807 if opts.js:
5808 CLANG_EXTS.append('.js')
5809
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005810 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5811 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5812 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005813 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005814
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005815 top_dir = os.path.normpath(
5816 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5817
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005818 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5819 # formatted. This is used to block during the presubmit.
5820 return_value = 0
5821
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005822 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005823 # Locate the clang-format binary in the checkout
5824 try:
5825 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005826 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005827 DieWithError(e)
5828
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005829 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005830 cmd = [clang_format_tool]
5831 if not opts.dry_run and not opts.diff:
5832 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005833 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005834 if opts.diff:
5835 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005836 else:
5837 env = os.environ.copy()
5838 env['PATH'] = str(os.path.dirname(clang_format_tool))
5839 try:
5840 script = clang_format.FindClangFormatScriptInChromiumTree(
5841 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005842 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005843 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005844
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005845 cmd = [sys.executable, script, '-p0']
5846 if not opts.dry_run and not opts.diff:
5847 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005848
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005849 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5850 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005851
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005852 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5853 if opts.diff:
5854 sys.stdout.write(stdout)
5855 if opts.dry_run and len(stdout) > 0:
5856 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005857
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005858 # Similar code to above, but using yapf on .py files rather than clang-format
5859 # on C/C++ files
5860 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005861 yapf_tool = gclient_utils.FindExecutable('yapf')
5862 if yapf_tool is None:
5863 DieWithError('yapf not found in PATH')
5864
5865 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005866 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005867 cmd = [yapf_tool]
5868 if not opts.dry_run and not opts.diff:
5869 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005870 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005871 if opts.diff:
5872 sys.stdout.write(stdout)
5873 else:
5874 # TODO(sbc): yapf --lines mode still has some issues.
5875 # https://github.com/google/yapf/issues/154
5876 DieWithError('--python currently only works with --full')
5877
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005878 # Dart's formatter does not have the nice property of only operating on
5879 # modified chunks, so hard code full.
5880 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005881 try:
5882 command = [dart_format.FindDartFmtToolInChromiumTree()]
5883 if not opts.dry_run and not opts.diff:
5884 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005885 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005886
ppi@chromium.org6593d932016-03-03 15:41:15 +00005887 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005888 if opts.dry_run and stdout:
5889 return_value = 2
5890 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005891 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5892 'found in this checkout. Files in other languages are still '
5893 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005894
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005895 # Format GN build files. Always run on full build files for canonical form.
5896 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005897 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005898 if opts.dry_run or opts.diff:
5899 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005900 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005901 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5902 shell=sys.platform == 'win32',
5903 cwd=top_dir)
5904 if opts.dry_run and gn_ret == 2:
5905 return_value = 2 # Not formatted.
5906 elif opts.diff and gn_ret == 2:
5907 # TODO this should compute and print the actual diff.
5908 print("This change has GN build file diff for " + gn_diff_file)
5909 elif gn_ret != 0:
5910 # For non-dry run cases (and non-2 return values for dry-run), a
5911 # nonzero error code indicates a failure, probably because the file
5912 # doesn't parse.
5913 DieWithError("gn format failed on " + gn_diff_file +
5914 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005915
Ilya Shermane081cbe2017-08-15 17:51:04 -07005916 # Skip the metrics formatting from the global presubmit hook. These files have
5917 # a separate presubmit hook that issues an error if the files need formatting,
5918 # whereas the top-level presubmit script merely issues a warning. Formatting
5919 # these files is somewhat slow, so it's important not to duplicate the work.
5920 if not opts.presubmit:
5921 for xml_dir in GetDirtyMetricsDirs(diff_files):
5922 tool_dir = os.path.join(top_dir, xml_dir)
5923 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5924 if opts.dry_run or opts.diff:
5925 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005926 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005927 if opts.diff:
5928 sys.stdout.write(stdout)
5929 if opts.dry_run and stdout:
5930 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005931
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005932 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005933
Steven Holte2e664bf2017-04-21 13:10:47 -07005934def GetDirtyMetricsDirs(diff_files):
5935 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5936 metrics_xml_dirs = [
5937 os.path.join('tools', 'metrics', 'actions'),
5938 os.path.join('tools', 'metrics', 'histograms'),
5939 os.path.join('tools', 'metrics', 'rappor'),
5940 os.path.join('tools', 'metrics', 'ukm')]
5941 for xml_dir in metrics_xml_dirs:
5942 if any(file.startswith(xml_dir) for file in xml_diff_files):
5943 yield xml_dir
5944
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005945
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005946@subcommand.usage('<codereview url or issue id>')
5947def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005948 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005949 _, args = parser.parse_args(args)
5950
5951 if len(args) != 1:
5952 parser.print_help()
5953 return 1
5954
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005955 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005956 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005957 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005958
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005959 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005960
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005961 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005962 output = RunGit(['config', '--local', '--get-regexp',
5963 r'branch\..*\.%s' % issueprefix],
5964 error_ok=True)
5965 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005966 if issue == target_issue:
5967 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005968
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005969 branches = []
5970 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005971 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005972 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005973 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005974 return 1
5975 if len(branches) == 1:
5976 RunGit(['checkout', branches[0]])
5977 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005978 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005979 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005980 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005981 which = raw_input('Choose by index: ')
5982 try:
5983 RunGit(['checkout', branches[int(which)]])
5984 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005985 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005986 return 1
5987
5988 return 0
5989
5990
maruel@chromium.org29404b52014-09-08 22:58:00 +00005991def CMDlol(parser, args):
5992 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005993 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005994 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5995 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5996 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005997 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005998 return 0
5999
6000
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006001class OptionParser(optparse.OptionParser):
6002 """Creates the option parse and add --verbose support."""
6003 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006004 optparse.OptionParser.__init__(
6005 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006006 self.add_option(
6007 '-v', '--verbose', action='count', default=0,
6008 help='Use 2 times for more debugging info')
6009
6010 def parse_args(self, args=None, values=None):
6011 options, args = optparse.OptionParser.parse_args(self, args, values)
6012 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006013 logging.basicConfig(
6014 level=levels[min(options.verbose, len(levels) - 1)],
6015 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6016 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006017 return options, args
6018
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006020def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006021 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006022 print('\nYour python version %s is unsupported, please upgrade.\n' %
6023 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006024 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006025
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006026 # Reload settings.
6027 global settings
6028 settings = Settings()
6029
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006030 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006031 dispatcher = subcommand.CommandDispatcher(__name__)
6032 try:
6033 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006034 except auth.AuthenticationError as e:
6035 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006036 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006037 if e.code != 500:
6038 raise
6039 DieWithError(
6040 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6041 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006042 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006043
6044
6045if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006046 # These affect sys.stdout so do it outside of main() to simplify mocks in
6047 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006048 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006049 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006050 try:
6051 sys.exit(main(sys.argv[1:]))
6052 except KeyboardInterrupt:
6053 sys.stderr.write('interrupted\n')
6054 sys.exit(1)