blob: c1968d19b9a79e93886d997c5c9ae44091f076de [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
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
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
piman@chromium.org336f9122014-09-04 02:16:55 +000059import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000060import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000062import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
tandrii9d2c7a32016-06-22 03:42:45 -070071COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070072DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080073POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000075REFS_THAT_ALIAS_TO_OTHER_REFS = {
76 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
77 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
78}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
thestig@chromium.org44202a22014-03-11 19:22:18 +000080# Valid extensions for files we want to lint.
81DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
82DEFAULT_LINT_IGNORE_REGEX = r"$^"
83
Aiden Bennerc08566e2018-10-03 17:52:42 +000084# File name for yapf style config files.
85YAPF_CONFIG_FILENAME = '.style.yapf'
86
borenet6c0efe62016-10-19 08:13:29 -070087# Buildbucket master name prefix.
88MASTER_PREFIX = 'master.'
89
Edward Lemur83bd7f42018-10-10 00:14:21 +000090# TODO(crbug.com/881860): Remove
91# Log gerrit failures to a gerrit_util.GERRIT_ERR_LOG_FILE.
92GERRIT_ERR_LOGGER = logging.getLogger('GerritErrorLogs')
93
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000094# Shortcut since it quickly becomes redundant.
95Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000096
maruel@chromium.orgddd59412011-11-30 14:20:38 +000097# Initialized in main()
98settings = None
99
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100100# Used by tests/git_cl_test.py to add extra logging.
101# Inside the weirdly failing test, add this:
102# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700103# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100104_IS_BEING_TESTED = False
105
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def DieWithError(message, change_desc=None):
108 if change_desc:
109 SaveDescriptionBackup(change_desc)
110
vapiera7fbd5a2016-06-16 09:17:49 -0700111 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112 sys.exit(1)
113
114
Christopher Lamf732cd52017-01-24 12:40:11 +1100115def SaveDescriptionBackup(change_desc):
116 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000117 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100118 backup_file = open(backup_path, 'w')
119 backup_file.write(change_desc.description)
120 backup_file.close()
121
122
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123def GetNoGitPagerEnv():
124 env = os.environ.copy()
125 # 'cat' is a magical git string that disables pagers on all platforms.
126 env['GIT_PAGER'] = 'cat'
127 return env
128
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000129
bsep@chromium.org627d9002016-04-29 00:00:52 +0000130def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000132 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000133 except subprocess2.CalledProcessError as e:
134 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000135 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 'Command "%s" failed.\n%s' % (
138 ' '.join(args), error_message or e.stdout or ''))
139 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000140
141
142def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000143 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000144 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000145
146
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000147def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000148 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700149 if suppress_stderr:
150 stderr = subprocess2.VOID
151 else:
152 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000153 try:
tandrii5d48c322016-08-18 16:19:37 -0700154 (out, _), code = subprocess2.communicate(['git'] + args,
155 env=GetNoGitPagerEnv(),
156 stdout=subprocess2.PIPE,
157 stderr=stderr)
158 return code, out
159 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900160 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700161 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162
163
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000164def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000165 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000166 return RunGitWithCode(args, suppress_stderr=True)[1]
167
168
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000169def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000170 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000171 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000172 return (version.startswith(prefix) and
173 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000174
175
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000176def BranchExists(branch):
177 """Return True if specified branch exists."""
178 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
179 suppress_stderr=True)
180 return not code
181
182
tandrii2a16b952016-10-19 07:09:44 -0700183def time_sleep(seconds):
184 # Use this so that it can be mocked in tests without interfering with python
185 # system machinery.
186 import time # Local import to discourage others from importing time globally.
187 return time.sleep(seconds)
188
189
maruel@chromium.org90541732011-04-01 17:54:18 +0000190def ask_for_data(prompt):
191 try:
192 return raw_input(prompt)
193 except KeyboardInterrupt:
194 # Hide the exception.
195 sys.exit(1)
196
197
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100198def confirm_or_exit(prefix='', action='confirm'):
199 """Asks user to press enter to continue or press Ctrl+C to abort."""
200 if not prefix or prefix.endswith('\n'):
201 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100202 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100203 mid = ' Press'
204 elif prefix.endswith(' '):
205 mid = 'press'
206 else:
207 mid = ' press'
208 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
209
210
211def ask_for_explicit_yes(prompt):
212 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
213 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
214 while True:
215 if 'yes'.startswith(result):
216 return True
217 if 'no'.startswith(result):
218 return False
219 result = ask_for_data('Please, type yes or no: ').lower()
220
221
tandrii5d48c322016-08-18 16:19:37 -0700222def _git_branch_config_key(branch, key):
223 """Helper method to return Git config key for a branch."""
224 assert branch, 'branch name is required to set git config for it'
225 return 'branch.%s.%s' % (branch, key)
226
227
228def _git_get_branch_config_value(key, default=None, value_type=str,
229 branch=False):
230 """Returns git config value of given or current branch if any.
231
232 Returns default in all other cases.
233 """
234 assert value_type in (int, str, bool)
235 if branch is False: # Distinguishing default arg value from None.
236 branch = GetCurrentBranch()
237
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000238 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700239 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000240
tandrii5d48c322016-08-18 16:19:37 -0700241 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700242 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700243 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700244 # git config also has --int, but apparently git config suffers from integer
245 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700246 args.append(_git_branch_config_key(branch, key))
247 code, out = RunGitWithCode(args)
248 if code == 0:
249 value = out.strip()
250 if value_type == int:
251 return int(value)
252 if value_type == bool:
253 return bool(value.lower() == 'true')
254 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 return default
256
257
tandrii5d48c322016-08-18 16:19:37 -0700258def _git_set_branch_config_value(key, value, branch=None, **kwargs):
259 """Sets the value or unsets if it's None of a git branch config.
260
261 Valid, though not necessarily existing, branch must be provided,
262 otherwise currently checked out branch is used.
263 """
264 if not branch:
265 branch = GetCurrentBranch()
266 assert branch, 'a branch name OR currently checked out branch is required'
267 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700268 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700269 if value is None:
270 args.append('--unset')
271 elif isinstance(value, bool):
272 args.append('--bool')
273 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700274 else:
tandrii33a46ff2016-08-23 05:53:40 -0700275 # git config also has --int, but apparently git config suffers from integer
276 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700277 value = str(value)
278 args.append(_git_branch_config_key(branch, key))
279 if value is not None:
280 args.append(value)
281 RunGit(args, **kwargs)
282
283
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100284def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700285 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100286
287 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
288 """
289 # Git also stores timezone offset, but it only affects visual display,
290 # actual point in time is defined by this timestamp only.
291 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
292
293
294def _git_amend_head(message, committer_timestamp):
295 """Amends commit with new message and desired committer_timestamp.
296
297 Sets committer timezone to UTC.
298 """
299 env = os.environ.copy()
300 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
301 return RunGit(['commit', '--amend', '-m', message], env=env)
302
303
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304def _get_properties_from_options(options):
305 properties = dict(x.split('=', 1) for x in options.properties)
306 for key, val in properties.iteritems():
307 try:
308 properties[key] = json.loads(val)
309 except ValueError:
310 pass # If a value couldn't be evaluated, treat it as a string.
311 return properties
312
313
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000314def _prefix_master(master):
315 """Convert user-specified master name to full master name.
316
317 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
318 name, while the developers always use shortened master name
319 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
320 function does the conversion for buildbucket migration.
321 """
borenet6c0efe62016-10-19 08:13:29 -0700322 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 return master
borenet6c0efe62016-10-19 08:13:29 -0700324 return '%s%s' % (MASTER_PREFIX, master)
325
326
327def _unprefix_master(bucket):
328 """Convert bucket name to shortened master name.
329
330 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
331 name, while the developers always use shortened master name
332 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
333 function does the conversion for buildbucket migration.
334 """
335 if bucket.startswith(MASTER_PREFIX):
336 return bucket[len(MASTER_PREFIX):]
337 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338
339
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000340def _buildbucket_retry(operation_name, http, *args, **kwargs):
341 """Retries requests to buildbucket service and returns parsed json content."""
342 try_count = 0
343 while True:
344 response, content = http.request(*args, **kwargs)
345 try:
346 content_json = json.loads(content)
347 except ValueError:
348 content_json = None
349
350 # Buildbucket could return an error even if status==200.
351 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000352 error = content_json.get('error')
353 if error.get('code') == 403:
354 raise BuildbucketResponseException(
355 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000357 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 raise BuildbucketResponseException(msg)
359
360 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700361 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(
363 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700364 'Please file bugs at http://crbug.com, '
365 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 content)
367 return content_json
368 if response.status < 500 or try_count >= 2:
369 raise httplib2.HttpLib2Error(content)
370
371 # status >= 500 means transient failures.
372 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700373 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 try_count += 1
375 assert False, 'unreachable'
376
377
qyearsley1fdfcb62016-10-24 13:22:03 -0700378def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700379 """Returns a dict mapping bucket names to builders and tests,
380 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 """
qyearsleydd49f942016-10-28 11:57:22 -0700382 # If no bots are listed, we try to get a set of builders and tests based
383 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700384 if not options.bot:
385 change = changelist.GetChange(
386 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700387 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700388 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700389 change=change,
390 changed_files=change.LocalPaths(),
391 repository_root=settings.GetRoot(),
392 default_presubmit=None,
393 project=None,
394 verbose=options.verbose,
395 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700396 if masters is None:
397 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100398 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700399
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 if options.bucket:
401 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700402 if options.master:
403 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700404
qyearsleydd49f942016-10-28 11:57:22 -0700405 # If bots are listed but no master or bucket, then we need to find out
406 # the corresponding master for each bot.
407 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
408 if error_message:
409 option_parser.error(
410 'Tryserver master cannot be found because: %s\n'
411 'Please manually specify the tryserver master, e.g. '
412 '"-m tryserver.chromium.linux".' % error_message)
413 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700414
415
qyearsley123a4682016-10-26 09:12:17 -0700416def _get_bucket_map_for_builders(builders):
417 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700418 map_url = 'https://builders-map.appspot.com/'
419 try:
qyearsley123a4682016-10-26 09:12:17 -0700420 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700421 except urllib2.URLError as e:
422 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
423 (map_url, e))
424 except ValueError as e:
425 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700426 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 return None, 'Failed to build master map.'
428
qyearsley123a4682016-10-26 09:12:17 -0700429 bucket_map = {}
430 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800431 bucket = builders_map.get(builder, {}).get('bucket')
432 if bucket:
433 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700434 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700435
436
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800437def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700438 """Sends a request to Buildbucket to trigger try jobs for a changelist.
439
440 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700441 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 changelist: Changelist that the try jobs are associated with.
443 buckets: A nested dict mapping bucket names to builders to tests.
444 options: Command-line options.
445 """
tandriide281ae2016-10-12 06:02:30 -0700446 assert changelist.GetIssue(), 'CL must be uploaded first'
447 codereview_url = changelist.GetCodereviewServer()
448 assert codereview_url, 'CL must be uploaded first'
449 patchset = patchset or changelist.GetMostRecentPatchset()
450 assert patchset, 'CL must be uploaded first'
451
452 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700453 # Cache the buildbucket credentials under the codereview host key, so that
454 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700455 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 http = authenticator.authorize(httplib2.Http())
457 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 buildbucket_put_url = (
460 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000461 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000462 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700463 hostname=codereview_host,
464 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700466
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700467 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800468 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700469 if options.clobber:
470 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700471 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700472 if extra_properties:
473 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474
475 batch_req_body = {'builds': []}
476 print_text = []
477 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700478 for bucket, builders_and_tests in sorted(buckets.iteritems()):
479 print_text.append('Bucket: %s' % bucket)
480 master = None
481 if bucket.startswith(MASTER_PREFIX):
482 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 for builder, tests in sorted(builders_and_tests.iteritems()):
484 print_text.append(' %s: %s' % (builder, tests))
485 parameters = {
486 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000487 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100488 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'revision': options.revision,
490 }],
tandrii8c5a3532016-11-04 07:52:02 -0700491 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000492 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000493 if 'presubmit' in builder.lower():
494 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000495 if tests:
496 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700497
498 tags = [
499 'builder:%s' % builder,
500 'buildset:%s' % buildset,
501 'user_agent:git_cl_try',
502 ]
503 if master:
504 parameters['properties']['master'] = master
505 tags.append('master:%s' % master)
506
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 batch_req_body['builds'].append(
508 {
509 'bucket': bucket,
510 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700512 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 }
514 )
515
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000516 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700517 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 http,
519 buildbucket_put_url,
520 'PUT',
521 body=json.dumps(batch_req_body),
522 headers={'Content-Type': 'application/json'}
523 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000524 print_text.append('To see results here, run: git cl try-results')
525 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700526 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000527
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000528
tandrii221ab252016-10-06 08:12:04 -0700529def fetch_try_jobs(auth_config, changelist, buildbucket_host,
530 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700531 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000532
qyearsley53f48a12016-09-01 10:45:13 -0700533 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534 """
tandrii221ab252016-10-06 08:12:04 -0700535 assert buildbucket_host
536 assert changelist.GetIssue(), 'CL must be uploaded first'
537 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
538 patchset = patchset or changelist.GetMostRecentPatchset()
539 assert patchset, 'CL must be uploaded first'
540
541 codereview_url = changelist.GetCodereviewServer()
542 codereview_host = urlparse.urlparse(codereview_url).hostname
543 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 if authenticator.has_cached_credentials():
545 http = authenticator.authorize(httplib2.Http())
546 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700547 print('Warning: Some results might be missing because %s' %
548 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700549 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 http = httplib2.Http()
551
552 http.force_exception_to_status_code = True
553
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000554 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700557 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params = {'tag': 'buildset:%s' % buildset}
559
560 builds = {}
561 while True:
562 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700563 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700565 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 for build in content.get('builds', []):
567 builds[build['id']] = build
568 if 'next_cursor' in content:
569 params['start_cursor'] = content['next_cursor']
570 else:
571 break
572 return builds
573
574
qyearsleyeab3c042016-08-24 09:18:28 -0700575def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 """Prints nicely result of fetch_try_jobs."""
577 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700578 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 return
580
581 # Make a copy, because we'll be modifying builds dictionary.
582 builds = builds.copy()
583 builder_names_cache = {}
584
585 def get_builder(b):
586 try:
587 return builder_names_cache[b['id']]
588 except KeyError:
589 try:
590 parameters = json.loads(b['parameters_json'])
591 name = parameters['builder_name']
592 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700593 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700594 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000595 name = None
596 builder_names_cache[b['id']] = name
597 return name
598
599 def get_bucket(b):
600 bucket = b['bucket']
601 if bucket.startswith('master.'):
602 return bucket[len('master.'):]
603 return bucket
604
605 if options.print_master:
606 name_fmt = '%%-%ds %%-%ds' % (
607 max(len(str(get_bucket(b))) for b in builds.itervalues()),
608 max(len(str(get_builder(b))) for b in builds.itervalues()))
609 def get_name(b):
610 return name_fmt % (get_bucket(b), get_builder(b))
611 else:
612 name_fmt = '%%-%ds' % (
613 max(len(str(get_builder(b))) for b in builds.itervalues()))
614 def get_name(b):
615 return name_fmt % get_builder(b)
616
617 def sort_key(b):
618 return b['status'], b.get('result'), get_name(b), b.get('url')
619
620 def pop(title, f, color=None, **kwargs):
621 """Pop matching builds from `builds` dict and print them."""
622
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000623 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000624 colorize = str
625 else:
626 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
627
628 result = []
629 for b in builds.values():
630 if all(b.get(k) == v for k, v in kwargs.iteritems()):
631 builds.pop(b['id'])
632 result.append(b)
633 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700634 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000635 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637
638 total = len(builds)
639 pop(status='COMPLETED', result='SUCCESS',
640 title='Successes:', color=Fore.GREEN,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
643 title='Infra Failures:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b), b.get('url')))
645 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
646 title='Failures:', color=Fore.RED,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='CANCELED',
649 title='Canceled:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b),))
651 pop(status='COMPLETED', result='FAILURE',
652 failure_reason='INVALID_BUILD_DEFINITION',
653 title='Wrong master/builder name:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 title='Other failures:',
657 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
658 pop(status='COMPLETED',
659 title='Other finished:',
660 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
661 pop(status='STARTED',
662 title='Started:', color=Fore.YELLOW,
663 f=lambda b: (get_name(b), b.get('url')))
664 pop(status='SCHEDULED',
665 title='Scheduled:',
666 f=lambda b: (get_name(b), 'id=%s' % b['id']))
667 # The last section is just in case buildbucket API changes OR there is a bug.
668 pop(title='Other:',
669 f=lambda b: (get_name(b), 'id=%s' % b['id']))
670 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700671 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674def _ComputeDiffLineRanges(files, upstream_commit):
675 """Gets the changed line ranges for each file since upstream_commit.
676
677 Parses a git diff on provided files and returns a dict that maps a file name
678 to an ordered list of range tuples in the form (start_line, count).
679 Ranges are in the same format as a git diff.
680 """
681 # If files is empty then diff_output will be a full diff.
682 if len(files) == 0:
683 return {}
684
685 # Take diff and find the line ranges where there are changes.
686 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
687 diff_output = RunGit(diff_cmd)
688
689 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
690 # 2 capture groups
691 # 0 == fname of diff file
692 # 1 == 'diff_start,diff_count' or 'diff_start'
693 # will match each of
694 # diff --git a/foo.foo b/foo.py
695 # @@ -12,2 +14,3 @@
696 # @@ -12,2 +17 @@
697 # running re.findall on the above string with pattern will give
698 # [('foo.py', ''), ('', '14,3'), ('', '17')]
699
700 curr_file = None
701 line_diffs = {}
702 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
703 if match[0] != '':
704 # Will match the second filename in diff --git a/a.py b/b.py.
705 curr_file = match[0]
706 line_diffs[curr_file] = []
707 else:
708 # Matches +14,3
709 if ',' in match[1]:
710 diff_start, diff_count = match[1].split(',')
711 else:
712 # Single line changes are of the form +12 instead of +12,1.
713 diff_start = match[1]
714 diff_count = 1
715
716 diff_start = int(diff_start)
717 diff_count = int(diff_count)
718
719 # If diff_count == 0 this is a removal we can ignore.
720 line_diffs[curr_file].append((diff_start, diff_count))
721
722 return line_diffs
723
724
725def _FindYapfConfigFile(fpath,
726 yapf_config_cache,
727 top_dir=None,
728 default_style=None):
729 """Checks if a yapf file is in any parent directory of fpath until top_dir.
730
731 Recursively checks parent directories to find yapf file
732 and if no yapf file is found returns default_style.
733 Uses yapf_config_cache as a cache for previously found files.
734 """
735 # Return result if we've already computed it.
736 if fpath in yapf_config_cache:
737 return yapf_config_cache[fpath]
738
739 # Check if there is a style file in the current directory.
740 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
741 dirname = os.path.dirname(fpath)
742 if os.path.isfile(yapf_file):
743 ret = yapf_file
744 elif fpath == top_dir or dirname == fpath:
745 # If we're at the top level directory, or if we're at root
746 # use the chromium default yapf style.
747 ret = default_style
748 else:
749 # Otherwise recurse on the current directory.
750 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
751 default_style)
752 yapf_config_cache[fpath] = ret
753 return ret
754
755
qyearsley53f48a12016-09-01 10:45:13 -0700756def write_try_results_json(output_file, builds):
757 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
758
759 The input |builds| dict is assumed to be generated by Buildbucket.
760 Buildbucket documentation: http://goo.gl/G0s101
761 """
762
763 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800764 """Extracts some of the information from one build dict."""
765 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700766 return {
767 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700768 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800769 'builder_name': parameters.get('builder_name'),
770 'created_ts': build.get('created_ts'),
771 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700772 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800773 'result': build.get('result'),
774 'status': build.get('status'),
775 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700776 'url': build.get('url'),
777 }
778
779 converted = []
780 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700782 write_json(output_file, converted)
783
784
Aaron Gable13101a62018-02-09 13:20:41 -0800785def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000786 """Prints statistics about the change to the user."""
787 # --no-ext-diff is broken in some versions of Git, so try to work around
788 # this by overriding the environment (but there is still a problem if the
789 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000790 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000791 if 'GIT_EXTERNAL_DIFF' in env:
792 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000793
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000794 try:
795 stdout = sys.stdout.fileno()
796 except AttributeError:
797 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000798 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800799 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000801
802
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000803class BuildbucketResponseException(Exception):
804 pass
805
806
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807class Settings(object):
808 def __init__(self):
809 self.default_server = None
810 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000811 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 self.tree_status_url = None
813 self.viewvc_url = None
814 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000815 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000816 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000817 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000818 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000819 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000820 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 def LazyUpdateIfNeeded(self):
823 """Updates the settings from a codereview.settings file, if available."""
824 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000825 # The only value that actually changes the behavior is
826 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000827 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000828 error_ok=True
829 ).strip().lower()
830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000831 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000832 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 LoadCodereviewSettingsFromFile(cr_settings_file)
834 self.updated = True
835
836 def GetDefaultServerUrl(self, error_ok=False):
837 if not self.default_server:
838 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000839 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 if error_ok:
842 return self.default_server
843 if not self.default_server:
844 error_message = ('Could not find settings file. You must configure '
845 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000846 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000847 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 return self.default_server
849
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000850 @staticmethod
851 def GetRelativeRoot():
852 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000855 if self.root is None:
856 self.root = os.path.abspath(self.GetRelativeRoot())
857 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000859 def GetGitMirror(self, remote='origin'):
860 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000861 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000862 if not os.path.isdir(local_url):
863 return None
864 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
865 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100866 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100867 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000868 if mirror.exists():
869 return mirror
870 return None
871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 def GetTreeStatusUrl(self, error_ok=False):
873 if not self.tree_status_url:
874 error_message = ('You must configure your tree status URL by running '
875 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000876 self.tree_status_url = self._GetRietveldConfig(
877 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 return self.tree_status_url
879
880 def GetViewVCUrl(self):
881 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000882 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 return self.viewvc_url
884
rmistry@google.com90752582014-01-14 21:04:50 +0000885 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000886 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000887
rmistry@google.com78948ed2015-07-08 23:09:57 +0000888 def GetIsSkipDependencyUpload(self, branch_name):
889 """Returns true if specified branch should skip dep uploads."""
890 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
891 error_ok=True)
892
rmistry@google.com5626a922015-02-26 14:03:30 +0000893 def GetRunPostUploadHook(self):
894 run_post_upload_hook = self._GetRietveldConfig(
895 'run-post-upload-hook', error_ok=True)
896 return run_post_upload_hook == "True"
897
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000898 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000899 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000900
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000901 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000902 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000903
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700905 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000906 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700907 self.is_gerrit = (
908 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000909 return self.is_gerrit
910
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000911 def GetSquashGerritUploads(self):
912 """Return true if uploads to Gerrit should be squashed by default."""
913 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700914 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
915 if self.squash_gerrit_uploads is None:
916 # Default is squash now (http://crbug.com/611892#c23).
917 self.squash_gerrit_uploads = not (
918 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
919 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000920 return self.squash_gerrit_uploads
921
tandriia60502f2016-06-20 02:01:53 -0700922 def GetSquashGerritUploadsOverride(self):
923 """Return True or False if codereview.settings should be overridden.
924
925 Returns None if no override has been defined.
926 """
927 # See also http://crbug.com/611892#c23
928 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
929 error_ok=True).strip()
930 if result == 'true':
931 return True
932 if result == 'false':
933 return False
934 return None
935
tandrii@chromium.org28253532016-04-14 13:46:56 +0000936 def GetGerritSkipEnsureAuthenticated(self):
937 """Return True if EnsureAuthenticated should not be done for Gerrit
938 uploads."""
939 if self.gerrit_skip_ensure_authenticated is None:
940 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000941 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000942 error_ok=True).strip() == 'true')
943 return self.gerrit_skip_ensure_authenticated
944
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000945 def GetGitEditor(self):
946 """Return the editor specified in the git config, or None if none is."""
947 if self.git_editor is None:
948 self.git_editor = self._GetConfig('core.editor', error_ok=True)
949 return self.git_editor or None
950
thestig@chromium.org44202a22014-03-11 19:22:18 +0000951 def GetLintRegex(self):
952 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
953 DEFAULT_LINT_REGEX)
954
955 def GetLintIgnoreRegex(self):
956 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
957 DEFAULT_LINT_IGNORE_REGEX)
958
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000959 def GetProject(self):
960 if not self.project:
961 self.project = self._GetRietveldConfig('project', error_ok=True)
962 return self.project
963
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000964 def _GetRietveldConfig(self, param, **kwargs):
965 return self._GetConfig('rietveld.' + param, **kwargs)
966
rmistry@google.com78948ed2015-07-08 23:09:57 +0000967 def _GetBranchConfig(self, branch_name, param, **kwargs):
968 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 def _GetConfig(self, param, **kwargs):
971 self.LazyUpdateIfNeeded()
972 return RunGit(['config', param], **kwargs).strip()
973
974
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100975@contextlib.contextmanager
976def _get_gerrit_project_config_file(remote_url):
977 """Context manager to fetch and store Gerrit's project.config from
978 refs/meta/config branch and store it in temp file.
979
980 Provides a temporary filename or None if there was error.
981 """
982 error, _ = RunGitWithCode([
983 'fetch', remote_url,
984 '+refs/meta/config:refs/git_cl/meta/config'])
985 if error:
986 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700987 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100988 (remote_url, error))
989 yield None
990 return
991
992 error, project_config_data = RunGitWithCode(
993 ['show', 'refs/git_cl/meta/config:project.config'])
994 if error:
995 print('WARNING: project.config file not found')
996 yield None
997 return
998
999 with gclient_utils.temporary_directory() as tempdir:
1000 project_config_file = os.path.join(tempdir, 'project.config')
1001 gclient_utils.FileWrite(project_config_file, project_config_data)
1002 yield project_config_file
1003
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005def ShortBranchName(branch):
1006 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 return branch.replace('refs/heads/', '', 1)
1008
1009
1010def GetCurrentBranchRef():
1011 """Returns branch ref (e.g., refs/heads/master) or None."""
1012 return RunGit(['symbolic-ref', 'HEAD'],
1013 stderr=subprocess2.VOID, error_ok=True).strip() or None
1014
1015
1016def GetCurrentBranch():
1017 """Returns current branch or None.
1018
1019 For refs/heads/* branches, returns just last part. For others, full ref.
1020 """
1021 branchref = GetCurrentBranchRef()
1022 if branchref:
1023 return ShortBranchName(branchref)
1024 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025
1026
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001027class _CQState(object):
1028 """Enum for states of CL with respect to Commit Queue."""
1029 NONE = 'none'
1030 DRY_RUN = 'dry_run'
1031 COMMIT = 'commit'
1032
1033 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1034
1035
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001036class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001037 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038 self.issue = issue
1039 self.patchset = patchset
1040 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001041 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001042 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001043
1044 @property
1045 def valid(self):
1046 return self.issue is not None
1047
1048
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001049def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1051 fail_result = _ParsedIssueNumberArgument()
1052
1053 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001054 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 if not arg.startswith('http'):
1056 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001058 url = gclient_utils.UpgradeToHttps(arg)
1059 try:
1060 parsed_url = urlparse.urlparse(url)
1061 except ValueError:
1062 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001063
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001064 if codereview is not None:
1065 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1066 return parsed or fail_result
1067
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001068 results = {}
1069 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1070 parsed = cls.ParseIssueURL(parsed_url)
1071 if parsed is not None:
1072 results[name] = parsed
1073
1074 if not results:
1075 return fail_result
1076 if len(results) == 1:
1077 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001078
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001079 return results['gerrit']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001080
1081
Aaron Gablea45ee112016-11-22 15:14:38 -08001082class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001083 def __init__(self, issue, url):
1084 self.issue = issue
1085 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001086 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001087
1088 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001089 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001090 self.issue, self.url)
1091
1092
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001093_CommentSummary = collections.namedtuple(
1094 '_CommentSummary', ['date', 'message', 'sender',
1095 # TODO(tandrii): these two aren't known in Gerrit.
1096 'approval', 'disapproval'])
1097
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001100 """Changelist works with one changelist in local branch.
1101
1102 Supports two codereview backends: Rietveld or Gerrit, selected at object
1103 creation.
1104
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001105 Notes:
1106 * Not safe for concurrent multi-{thread,process} use.
1107 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001108 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 """
1110
1111 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1112 """Create a new ChangeList instance.
1113
1114 If issue is given, the codereview must be given too.
1115
1116 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1117 Otherwise, it's decided based on current configuration of the local branch,
1118 with default being 'rietveld' for backwards compatibility.
1119 See _load_codereview_impl for more details.
1120
1121 **kwargs will be passed directly to codereview implementation.
1122 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001124 global settings
1125 if not settings:
1126 # Happens when git_cl.py is used as a utility library.
1127 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128
1129 if issue:
1130 assert codereview, 'codereview must be known, if issue is known'
1131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.branchref = branchref
1133 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001134 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 self.branch = ShortBranchName(self.branchref)
1136 else:
1137 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.lookedup_issue = False
1140 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 self.has_description = False
1142 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001143 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001146 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001147 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001148 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001149
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001152 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001153 assert self._codereview_impl
1154 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001155
1156 def _load_codereview_impl(self, codereview=None, **kwargs):
1157 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001158 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1159 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1160 self._codereview = codereview
1161 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001162 return
1163
1164 # Automatic selection based on issue number set for a current branch.
1165 # Rietveld takes precedence over Gerrit.
1166 assert not self.issue
1167 # Whether we find issue or not, we are doing the lookup.
1168 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001169 if self.GetBranch():
1170 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1171 issue = _git_get_branch_config_value(
1172 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1173 if issue:
1174 self._codereview = codereview
1175 self._codereview_impl = cls(self, **kwargs)
1176 self.issue = int(issue)
1177 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178
1179 # No issue is set for this branch, so decide based on repo-wide settings.
1180 return self._load_codereview_impl(
1181 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1182 **kwargs)
1183
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001184 def IsGerrit(self):
1185 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186
1187 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001188 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001189
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001190 The return value is a string suitable for passing to git cl with the --cc
1191 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192 """
1193 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001194 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001195 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1197 return self.cc
1198
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001199 def GetCCListWithoutDefault(self):
1200 """Return the users cc'd on this CL excluding default ones."""
1201 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001202 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001203 return self.cc
1204
Daniel Cheng7227d212017-11-17 08:12:37 -08001205 def ExtendCC(self, more_cc):
1206 """Extends the list of users to cc on this CL based on the changed files."""
1207 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208
1209 def GetBranch(self):
1210 """Returns the short branch name, e.g. 'master'."""
1211 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001213 if not branchref:
1214 return None
1215 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 self.branch = ShortBranchName(self.branchref)
1217 return self.branch
1218
1219 def GetBranchRef(self):
1220 """Returns the full branch name, e.g. 'refs/heads/master'."""
1221 self.GetBranch() # Poke the lazy loader.
1222 return self.branchref
1223
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001224 def ClearBranch(self):
1225 """Clears cached branch data of this object."""
1226 self.branch = self.branchref = None
1227
tandrii5d48c322016-08-18 16:19:37 -07001228 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1229 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1230 kwargs['branch'] = self.GetBranch()
1231 return _git_get_branch_config_value(key, default, **kwargs)
1232
1233 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1234 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1235 assert self.GetBranch(), (
1236 'this CL must have an associated branch to %sset %s%s' %
1237 ('un' if value is None else '',
1238 key,
1239 '' if value is None else ' to %r' % value))
1240 kwargs['branch'] = self.GetBranch()
1241 return _git_set_branch_config_value(key, value, **kwargs)
1242
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001243 @staticmethod
1244 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001245 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 e.g. 'origin', 'refs/heads/master'
1247 """
1248 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001249 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001252 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001254 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1255 error_ok=True).strip()
1256 if upstream_branch:
1257 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001259 # Else, try to guess the origin remote.
1260 remote_branches = RunGit(['branch', '-r']).split()
1261 if 'origin/master' in remote_branches:
1262 # Fall back on origin/master if it exits.
1263 remote = 'origin'
1264 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001266 DieWithError(
1267 'Unable to determine default branch to diff against.\n'
1268 'Either pass complete "git diff"-style arguments, like\n'
1269 ' git cl upload origin/master\n'
1270 'or verify this branch is set up to track another \n'
1271 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
1273 return remote, upstream_branch
1274
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001276 upstream_branch = self.GetUpstreamBranch()
1277 if not BranchExists(upstream_branch):
1278 DieWithError('The upstream for the current branch (%s) does not exist '
1279 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001280 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001281 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001283 def GetUpstreamBranch(self):
1284 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001285 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001287 upstream_branch = upstream_branch.replace('refs/heads/',
1288 'refs/remotes/%s/' % remote)
1289 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1290 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 self.upstream_branch = upstream_branch
1292 return self.upstream_branch
1293
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001294 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001295 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 remote, branch = None, self.GetBranch()
1297 seen_branches = set()
1298 while branch not in seen_branches:
1299 seen_branches.add(branch)
1300 remote, branch = self.FetchUpstreamTuple(branch)
1301 branch = ShortBranchName(branch)
1302 if remote != '.' or branch.startswith('refs/remotes'):
1303 break
1304 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001305 remotes = RunGit(['remote'], error_ok=True).split()
1306 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001307 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001308 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001310 logging.warn('Could not determine which remote this change is '
1311 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001312 else:
1313 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001314 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 branch = 'HEAD'
1316 if branch.startswith('refs/remotes'):
1317 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001318 elif branch.startswith('refs/branch-heads/'):
1319 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 else:
1321 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001322 return self._remote
1323
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001324 def GitSanityChecks(self, upstream_git_obj):
1325 """Checks git repo status and ensures diff is from local commits."""
1326
sbc@chromium.org79706062015-01-14 21:18:12 +00001327 if upstream_git_obj is None:
1328 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001329 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001330 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001331 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001332 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001333 return False
1334
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 # Verify the commit we're diffing against is in our current branch.
1336 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1337 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1338 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001339 print('ERROR: %s is not in the current branch. You may need to rebase '
1340 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 return False
1342
1343 # List the commits inside the diff, and verify they are all local.
1344 commits_in_diff = RunGit(
1345 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1346 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1347 remote_branch = remote_branch.strip()
1348 if code != 0:
1349 _, remote_branch = self.GetRemoteBranch()
1350
1351 commits_in_remote = RunGit(
1352 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1353
1354 common_commits = set(commits_in_diff) & set(commits_in_remote)
1355 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001356 print('ERROR: Your diff contains %d commits already in %s.\n'
1357 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1358 'the diff. If you are using a custom git flow, you can override'
1359 ' the reference used for this check with "git config '
1360 'gitcl.remotebranch <git-ref>".' % (
1361 len(common_commits), remote_branch, upstream_git_obj),
1362 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001363 return False
1364 return True
1365
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001366 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001367 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001368
1369 Returns None if it is not set.
1370 """
tandrii5d48c322016-08-18 16:19:37 -07001371 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001372
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 def GetRemoteUrl(self):
1374 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1375
1376 Returns None if there is no remote.
1377 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001378 is_cached, value = self._cached_remote_url
1379 if is_cached:
1380 return value
1381
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001382 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001383 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1384
1385 # If URL is pointing to a local directory, it is probably a git cache.
1386 if os.path.isdir(url):
1387 url = RunGit(['config', 'remote.%s.url' % remote],
1388 error_ok=True,
1389 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001390 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001391 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001393 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001394 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001395 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001396 self.issue = self._GitGetBranchConfigValue(
1397 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 return self.issue
1400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 def GetIssueURL(self):
1402 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001403 issue = self.GetIssue()
1404 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001405 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001406 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001408 def GetDescription(self, pretty=False, force=False):
1409 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001411 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 self.has_description = True
1413 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001414 # Set width to 72 columns + 2 space indent.
1415 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001417 lines = self.description.splitlines()
1418 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 return self.description
1420
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001421 def GetDescriptionFooters(self):
1422 """Returns (non_footer_lines, footers) for the commit message.
1423
1424 Returns:
1425 non_footer_lines (list(str)) - Simple list of description lines without
1426 any footer. The lines do not contain newlines, nor does the list contain
1427 the empty line between the message and the footers.
1428 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1429 [("Change-Id", "Ideadbeef...."), ...]
1430 """
1431 raw_description = self.GetDescription()
1432 msg_lines, _, footers = git_footers.split_footers(raw_description)
1433 if footers:
1434 msg_lines = msg_lines[:len(msg_lines)-1]
1435 return msg_lines, footers
1436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001438 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001439 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001440 self.patchset = self._GitGetBranchConfigValue(
1441 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001442 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.patchset
1444
1445 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001446 """Set this branch's patchset. If patchset=0, clears the patchset."""
1447 assert self.GetBranch()
1448 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001449 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001450 else:
1451 self.patchset = int(patchset)
1452 self._GitSetBranchConfigValue(
1453 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001455 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001456 """Set this branch's issue. If issue isn't given, clears the issue."""
1457 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001459 issue = int(issue)
1460 self._GitSetBranchConfigValue(
1461 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001462 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001463 codereview_server = self._codereview_impl.GetCodereviewServer()
1464 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001465 self._GitSetBranchConfigValue(
1466 self._codereview_impl.CodereviewServerConfigKey(),
1467 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 else:
tandrii5d48c322016-08-18 16:19:37 -07001469 # Reset all of these just to be clean.
1470 reset_suffixes = [
1471 'last-upload-hash',
1472 self._codereview_impl.IssueConfigKey(),
1473 self._codereview_impl.PatchsetConfigKey(),
1474 self._codereview_impl.CodereviewServerConfigKey(),
1475 ] + self._PostUnsetIssueProperties()
1476 for prop in reset_suffixes:
1477 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001478 msg = RunGit(['log', '-1', '--format=%B']).strip()
1479 if msg and git_footers.get_footer_change_id(msg):
1480 print('WARNING: The change patched into this branch has a Change-Id. '
1481 'Removing it.')
1482 RunGit(['commit', '--amend', '-m',
1483 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001484 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001485 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486
dnjba1b0f32016-09-02 12:37:42 -07001487 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001488 if not self.GitSanityChecks(upstream_branch):
1489 DieWithError('\nGit sanity check failure')
1490
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001491 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001492 if not root:
1493 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001494 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001495
1496 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001497 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001498 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001500 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001501 except subprocess2.CalledProcessError:
1502 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001503 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001504 'This branch probably doesn\'t exist anymore. To reset the\n'
1505 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001506 ' git branch --set-upstream-to origin/master %s\n'
1507 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001508 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001509
maruel@chromium.org52424302012-08-29 15:14:30 +00001510 issue = self.GetIssue()
1511 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001512 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001513 description = self.GetDescription()
1514 else:
1515 # If the change was never uploaded, use the log messages of all commits
1516 # up to the branch point, as git cl upload will prefill the description
1517 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001518 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1519 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001520
1521 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001522 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001523 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524 name,
1525 description,
1526 absroot,
1527 files,
1528 issue,
1529 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001530 author,
1531 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001532
dsansomee2d6fd92016-09-08 00:10:47 -07001533 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001534 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001535 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001536 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001537
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001538 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1539 """Sets the description for this CL remotely.
1540
1541 You can get description_lines and footers with GetDescriptionFooters.
1542
1543 Args:
1544 description_lines (list(str)) - List of CL description lines without
1545 newline characters.
1546 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1547 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1548 `List-Of-Tokens`). It will be case-normalized so that each token is
1549 title-cased.
1550 """
1551 new_description = '\n'.join(description_lines)
1552 if footers:
1553 new_description += '\n'
1554 for k, v in footers:
1555 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1556 if not git_footers.FOOTER_PATTERN.match(foot):
1557 raise ValueError('Invalid footer %r' % foot)
1558 new_description += foot + '\n'
1559 self.UpdateDescription(new_description, force)
1560
Edward Lesmes8e282792018-04-03 18:50:29 -04001561 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1563 try:
1564 return presubmit_support.DoPresubmitChecks(change, committing,
1565 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1566 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001567 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1568 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001569 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001570 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001571
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001572 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1573 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001574 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1575 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001576 else:
1577 # Assume url.
1578 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1579 urlparse.urlparse(issue_arg))
1580 if not parsed_issue_arg or not parsed_issue_arg.valid:
1581 DieWithError('Failed to parse issue argument "%s". '
1582 'Must be an issue number or a valid URL.' % issue_arg)
1583 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001584 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001585
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001586 def CMDUpload(self, options, git_diff_args, orig_args):
1587 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001588 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001590 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001591 else:
1592 if self.GetBranch() is None:
1593 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1594
1595 # Default to diffing against common ancestor of upstream branch
1596 base_branch = self.GetCommonAncestorWithUpstream()
1597 git_diff_args = [base_branch, 'HEAD']
1598
Aaron Gablec4c40d12017-05-22 11:49:53 -07001599 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1600 if not self.IsGerrit() and not self.GetIssue():
1601 print('=====================================')
1602 print('NOTICE: Rietveld is being deprecated. '
1603 'You can upload changes to Gerrit with')
1604 print(' git cl upload --gerrit')
1605 print('or set Gerrit to be your default code review tool with')
1606 print(' git config gerrit.host true')
1607 print('=====================================')
1608
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001609 # Fast best-effort checks to abort before running potentially
1610 # expensive hooks if uploading is likely to fail anyway. Passing these
1611 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001612 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001613 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614
1615 # Apply watchlists on upload.
1616 change = self.GetChange(base_branch, None)
1617 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1618 files = [f.LocalPath() for f in change.AffectedFiles()]
1619 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001620 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621
1622 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001623 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001624 # Set the reviewer list now so that presubmit checks can access it.
1625 change_description = ChangeDescription(change.FullDescriptionText())
1626 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001627 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001628 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629 change)
1630 change.SetDescriptionText(change_description.description)
1631 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001632 may_prompt=not options.force,
1633 verbose=options.verbose,
1634 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001635 if not hook_results.should_continue():
1636 return 1
1637 if not options.reviewers and hook_results.reviewers:
1638 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001639 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001640
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001641 # TODO(tandrii): Checking local patchset against remote patchset is only
1642 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1643 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644 latest_patchset = self.GetMostRecentPatchset()
1645 local_patchset = self.GetPatchset()
1646 if (latest_patchset and local_patchset and
1647 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001648 print('The last upload made from this repository was patchset #%d but '
1649 'the most recent patchset on the server is #%d.'
1650 % (local_patchset, latest_patchset))
1651 print('Uploading will still work, but if you\'ve uploaded to this '
1652 'issue from another machine or branch the patch you\'re '
1653 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001654 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001655
Aaron Gable13101a62018-02-09 13:20:41 -08001656 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001657 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001658 if not ret:
Andrii Shyshkalovdd788442018-10-13 17:55:29 +00001659 if not self.IsGerrit():
Ravi Mistry31e7d562018-04-02 12:53:57 -04001660 if options.use_commit_queue:
1661 self.SetCQState(_CQState.COMMIT)
1662 elif options.cq_dry_run:
1663 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001664
tandrii5d48c322016-08-18 16:19:37 -07001665 _git_set_branch_config_value('last-upload-hash',
1666 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001667 # Run post upload hooks, if specified.
1668 if settings.GetRunPostUploadHook():
1669 presubmit_support.DoPostUploadExecuter(
1670 change,
1671 self,
1672 settings.GetRoot(),
1673 options.verbose,
1674 sys.stdout)
1675
1676 # Upload all dependencies if specified.
1677 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001678 print()
1679 print('--dependencies has been specified.')
1680 print('All dependent local branches will be re-uploaded.')
1681 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001682 # Remove the dependencies flag from args so that we do not end up in a
1683 # loop.
1684 orig_args.remove('--dependencies')
1685 ret = upload_branch_deps(self, orig_args)
1686 return ret
1687
Ravi Mistry31e7d562018-04-02 12:53:57 -04001688 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1689 """Sets labels on the change based on the provided flags.
1690
1691 Sets labels if issue is already uploaded and known, else returns without
1692 doing anything.
1693
1694 Args:
1695 enable_auto_submit: Sets Auto-Submit+1 on the change.
1696 use_commit_queue: Sets Commit-Queue+2 on the change.
1697 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1698 both use_commit_queue and cq_dry_run are true.
1699 """
1700 if not self.GetIssue():
1701 return
1702 try:
1703 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1704 cq_dry_run)
1705 return 0
1706 except KeyboardInterrupt:
1707 raise
1708 except:
1709 labels = []
1710 if enable_auto_submit:
1711 labels.append('Auto-Submit')
1712 if use_commit_queue or cq_dry_run:
1713 labels.append('Commit-Queue')
1714 print('WARNING: Failed to set label(s) on your change: %s\n'
1715 'Either:\n'
1716 ' * Your project does not have the above label(s),\n'
1717 ' * You don\'t have permission to set the above label(s),\n'
1718 ' * There\'s a bug in this code (see stack trace below).\n' %
1719 (', '.join(labels)))
1720 # Still raise exception so that stack trace is printed.
1721 raise
1722
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001723 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001724 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001725
1726 Issue must have been already uploaded and known.
1727 """
1728 assert new_state in _CQState.ALL_STATES
1729 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001730 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001731 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001732 return 0
1733 except KeyboardInterrupt:
1734 raise
1735 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001736 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001737 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001738 ' * Your project has no CQ,\n'
1739 ' * You don\'t have permission to change the CQ state,\n'
1740 ' * There\'s a bug in this code (see stack trace below).\n'
1741 'Consider specifying which bots to trigger manually or asking your '
1742 'project owners for permissions or contacting Chrome Infra at:\n'
1743 'https://www.chromium.org/infra\n\n' %
1744 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001745 # Still raise exception so that stack trace is printed.
1746 raise
1747
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 # Forward methods to codereview specific implementation.
1749
Aaron Gable636b13f2017-07-14 10:42:48 -07001750 def AddComment(self, message, publish=None):
1751 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001752
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001753 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001754 """Returns list of _CommentSummary for each comment.
1755
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001756 args:
1757 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001758 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001759 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001760
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 def CloseIssue(self):
1762 return self._codereview_impl.CloseIssue()
1763
1764 def GetStatus(self):
1765 return self._codereview_impl.GetStatus()
1766
1767 def GetCodereviewServer(self):
1768 return self._codereview_impl.GetCodereviewServer()
1769
tandriide281ae2016-10-12 06:02:30 -07001770 def GetIssueOwner(self):
1771 """Get owner from codereview, which may differ from this checkout."""
1772 return self._codereview_impl.GetIssueOwner()
1773
Edward Lemur707d70b2018-02-07 00:50:14 +01001774 def GetReviewers(self):
1775 return self._codereview_impl.GetReviewers()
1776
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001777 def GetMostRecentPatchset(self):
1778 return self._codereview_impl.GetMostRecentPatchset()
1779
tandriide281ae2016-10-12 06:02:30 -07001780 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001781 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001782 return self._codereview_impl.CannotTriggerTryJobReason()
1783
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001784 def GetTryJobProperties(self, patchset=None):
1785 """Returns dictionary of properties to launch try job."""
1786 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001787
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001788 def __getattr__(self, attr):
1789 # This is because lots of untested code accesses Rietveld-specific stuff
1790 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001791 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001792 # Note that child method defines __getattr__ as well, and forwards it here,
1793 # because _RietveldChangelistImpl is not cleaned up yet, and given
1794 # deprecation of Rietveld, it should probably be just removed.
1795 # Until that time, avoid infinite recursion by bypassing __getattr__
1796 # of implementation class.
1797 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798
1799
1800class _ChangelistCodereviewBase(object):
1801 """Abstract base class encapsulating codereview specifics of a changelist."""
1802 def __init__(self, changelist):
1803 self._changelist = changelist # instance of Changelist
1804
1805 def __getattr__(self, attr):
1806 # Forward methods to changelist.
1807 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1808 # _RietveldChangelistImpl to avoid this hack?
1809 return getattr(self._changelist, attr)
1810
1811 def GetStatus(self):
1812 """Apply a rough heuristic to give a simple summary of an issue's review
1813 or CQ status, assuming adherence to a common workflow.
1814
1815 Returns None if no issue for this branch, or specific string keywords.
1816 """
1817 raise NotImplementedError()
1818
1819 def GetCodereviewServer(self):
1820 """Returns server URL without end slash, like "https://codereview.com"."""
1821 raise NotImplementedError()
1822
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001823 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 """Fetches and returns description from the codereview server."""
1825 raise NotImplementedError()
1826
tandrii5d48c322016-08-18 16:19:37 -07001827 @classmethod
1828 def IssueConfigKey(cls):
1829 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001830 raise NotImplementedError()
1831
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001832 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001833 def PatchsetConfigKey(cls):
1834 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001835 raise NotImplementedError()
1836
tandrii5d48c322016-08-18 16:19:37 -07001837 @classmethod
1838 def CodereviewServerConfigKey(cls):
1839 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840 raise NotImplementedError()
1841
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001842 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001843 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001844 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001845
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001846 def GetGerritObjForPresubmit(self):
1847 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1848 return None
1849
dsansomee2d6fd92016-09-08 00:10:47 -07001850 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001851 """Update the description on codereview site."""
1852 raise NotImplementedError()
1853
Aaron Gable636b13f2017-07-14 10:42:48 -07001854 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001855 """Posts a comment to the codereview site."""
1856 raise NotImplementedError()
1857
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001858 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001859 raise NotImplementedError()
1860
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001861 def CloseIssue(self):
1862 """Closes the issue."""
1863 raise NotImplementedError()
1864
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865 def GetMostRecentPatchset(self):
1866 """Returns the most recent patchset number from the codereview site."""
1867 raise NotImplementedError()
1868
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001869 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001870 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001871 """Fetches and applies the issue.
1872
1873 Arguments:
1874 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1875 reject: if True, reject the failed patch instead of switching to 3-way
1876 merge. Rietveld only.
1877 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1878 only.
1879 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001880 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001881 """
1882 raise NotImplementedError()
1883
1884 @staticmethod
1885 def ParseIssueURL(parsed_url):
1886 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1887 failed."""
1888 raise NotImplementedError()
1889
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001890 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001891 """Best effort check that user is authenticated with codereview server.
1892
1893 Arguments:
1894 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001895 refresh: whether to attempt to refresh credentials. Ignored if not
1896 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001897 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001898 raise NotImplementedError()
1899
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001900 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001901 """Best effort check that uploading isn't supposed to fail for predictable
1902 reasons.
1903
1904 This method should raise informative exception if uploading shouldn't
1905 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001906
1907 Arguments:
1908 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001909 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001910 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001911
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001912 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001913 """Uploads a change to codereview."""
1914 raise NotImplementedError()
1915
Ravi Mistry31e7d562018-04-02 12:53:57 -04001916 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1917 """Sets labels on the change based on the provided flags.
1918
1919 Issue must have been already uploaded and known.
1920 """
1921 raise NotImplementedError()
1922
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001923 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001924 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001925
1926 Issue must have been already uploaded and known.
1927 """
1928 raise NotImplementedError()
1929
tandriie113dfd2016-10-11 10:20:12 -07001930 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001931 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001932 raise NotImplementedError()
1933
tandriide281ae2016-10-12 06:02:30 -07001934 def GetIssueOwner(self):
1935 raise NotImplementedError()
1936
Edward Lemur707d70b2018-02-07 00:50:14 +01001937 def GetReviewers(self):
1938 raise NotImplementedError()
1939
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001940 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001941 raise NotImplementedError()
1942
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001943
1944class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001945
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001946 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 super(_RietveldChangelistImpl, self).__init__(changelist)
1948 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001949 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001950 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001952 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001953 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 self._props = None
1955 self._rpc_server = None
1956
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001957 def GetCodereviewServer(self):
1958 if not self._rietveld_server:
1959 # If we're on a branch then get the server potentially associated
1960 # with that branch.
1961 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001962 self._rietveld_server = gclient_utils.UpgradeToHttps(
1963 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001964 if not self._rietveld_server:
1965 self._rietveld_server = settings.GetDefaultServerUrl()
1966 return self._rietveld_server
1967
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001968 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001969 """Best effort check that user is authenticated with Rietveld server."""
1970 if self._auth_config.use_oauth2:
1971 authenticator = auth.get_authenticator_for_host(
1972 self.GetCodereviewServer(), self._auth_config)
1973 if not authenticator.has_cached_credentials():
1974 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001975 if refresh:
1976 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001977
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001978 def EnsureCanUploadPatchset(self, force):
1979 # No checks for Rietveld because we are deprecating Rietveld.
1980 pass
1981
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001982 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001983 issue = self.GetIssue()
1984 assert issue
1985 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001986 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001987 except urllib2.HTTPError as e:
1988 if e.code == 404:
1989 DieWithError(
1990 ('\nWhile fetching the description for issue %d, received a '
1991 '404 (not found)\n'
1992 'error. It is likely that you deleted this '
1993 'issue on the server. If this is the\n'
1994 'case, please run\n\n'
1995 ' git cl issue 0\n\n'
1996 'to clear the association with the deleted issue. Then run '
1997 'this command again.') % issue)
1998 else:
1999 DieWithError(
2000 '\nFailed to fetch issue description. HTTP error %d' % e.code)
2001 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07002002 print('Warning: Failed to retrieve CL description due to network '
2003 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002004 return ''
2005
2006 def GetMostRecentPatchset(self):
2007 return self.GetIssueProperties()['patchsets'][-1]
2008
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002009 def GetIssueProperties(self):
2010 if self._props is None:
2011 issue = self.GetIssue()
2012 if not issue:
2013 self._props = {}
2014 else:
2015 self._props = self.RpcServer().get_issue_properties(issue, True)
2016 return self._props
2017
tandriie113dfd2016-10-11 10:20:12 -07002018 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00002019 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07002020
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002021 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00002022 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07002023
tandriide281ae2016-10-12 06:02:30 -07002024 def GetIssueOwner(self):
2025 return (self.GetIssueProperties() or {}).get('owner_email')
2026
Edward Lemur707d70b2018-02-07 00:50:14 +01002027 def GetReviewers(self):
2028 return (self.GetIssueProperties() or {}).get('reviewers')
2029
Aaron Gable636b13f2017-07-14 10:42:48 -07002030 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002031 return self.RpcServer().add_comment(self.GetIssue(), message)
2032
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002033 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002034 summary = []
2035 for message in self.GetIssueProperties().get('messages', []):
2036 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2037 summary.append(_CommentSummary(
2038 date=date,
2039 disapproval=bool(message['disapproval']),
2040 approval=bool(message['approval']),
2041 sender=message['sender'],
2042 message=message['text'],
2043 ))
2044 return summary
2045
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002046 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002047 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002048 or CQ status, assuming adherence to a common workflow.
2049
2050 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002051 * 'error' - error from review tool (including deleted issues)
2052 * 'unsent' - not sent for review
2053 * 'waiting' - waiting for review
2054 * 'reply' - waiting for owner to reply to review
2055 * 'not lgtm' - Code-Review label has been set negatively
2056 * 'lgtm' - LGTM from at least one approved reviewer
2057 * 'commit' - in the commit queue
2058 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002059 """
2060 if not self.GetIssue():
2061 return None
2062
2063 try:
2064 props = self.GetIssueProperties()
2065 except urllib2.HTTPError:
2066 return 'error'
2067
2068 if props.get('closed'):
2069 # Issue is closed.
2070 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002071 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002072 # Issue is in the commit queue.
2073 return 'commit'
2074
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002075 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002076 if not messages:
2077 # No message was sent.
2078 return 'unsent'
2079
2080 if get_approving_reviewers(props):
2081 return 'lgtm'
2082 elif get_approving_reviewers(props, disapproval=True):
2083 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002084
tandrii9d2c7a32016-06-22 03:42:45 -07002085 # Skip CQ messages that don't require owner's action.
2086 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2087 if 'Dry run:' in messages[-1]['text']:
2088 messages.pop()
2089 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2090 # This message always follows prior messages from CQ,
2091 # so skip this too.
2092 messages.pop()
2093 else:
2094 # This is probably a CQ messages warranting user attention.
2095 break
2096
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002097 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002098 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002099 return 'reply'
2100 return 'waiting'
2101
dsansomee2d6fd92016-09-08 00:10:47 -07002102 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002103 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002106 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002108 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002109 return self.SetFlags({flag: value})
2110
2111 def SetFlags(self, flags):
2112 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002113 """
phajdan.jr68598232016-08-10 03:28:28 -07002114 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002115 try:
tandrii4b233bd2016-07-06 03:50:29 -07002116 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002117 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002118 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002119 if e.code == 404:
2120 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2121 if e.code == 403:
2122 DieWithError(
2123 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002124 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002125 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002127 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128 """Returns an upload.RpcServer() to access this review's rietveld instance.
2129 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002130 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002131 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002132 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002133 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002134 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002136 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002137 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002138 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139
tandrii5d48c322016-08-18 16:19:37 -07002140 @classmethod
2141 def PatchsetConfigKey(cls):
2142 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143
tandrii5d48c322016-08-18 16:19:37 -07002144 @classmethod
2145 def CodereviewServerConfigKey(cls):
2146 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002147
Ravi Mistry31e7d562018-04-02 12:53:57 -04002148 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2149 raise NotImplementedError()
2150
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002151 def SetCQState(self, new_state):
2152 props = self.GetIssueProperties()
2153 if props.get('private'):
2154 DieWithError('Cannot set-commit on private issue')
2155
2156 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002157 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002158 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002159 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002160 else:
tandrii4b233bd2016-07-06 03:50:29 -07002161 assert new_state == _CQState.DRY_RUN
2162 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002163
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002164 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002165 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002166 # PatchIssue should never be called with a dirty tree. It is up to the
2167 # caller to check this, but just in case we assert here since the
2168 # consequences of the caller not checking this could be dire.
2169 assert(not git_common.is_dirty_git_tree('apply'))
2170 assert(parsed_issue_arg.valid)
2171 self._changelist.issue = parsed_issue_arg.issue
2172 if parsed_issue_arg.hostname:
2173 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2174
skobes6468b902016-10-24 08:45:10 -07002175 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2176 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2177 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002178 try:
skobes6468b902016-10-24 08:45:10 -07002179 scm_obj.apply_patch(patchset_object)
2180 except Exception as e:
2181 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002182 return 1
2183
2184 # If we had an issue, commit the current state and register the issue.
2185 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002186 self.SetIssue(self.GetIssue())
2187 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002188 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2189 'patch from issue %(i)s at patchset '
2190 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2191 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002192 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002193 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002194 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 return 0
2196
2197 @staticmethod
2198 def ParseIssueURL(parsed_url):
2199 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2200 return None
wychen3c1c1722016-08-04 11:46:36 -07002201 # Rietveld patch: https://domain/<number>/#ps<patchset>
2202 match = re.match(r'/(\d+)/$', parsed_url.path)
2203 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2204 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002205 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002206 issue=int(match.group(1)),
2207 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002208 hostname=parsed_url.netloc,
2209 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002210 # Typical url: https://domain/<issue_number>[/[other]]
2211 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2212 if match:
skobes6468b902016-10-24 08:45:10 -07002213 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002214 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002215 hostname=parsed_url.netloc,
2216 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002217 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2218 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2219 if match:
skobes6468b902016-10-24 08:45:10 -07002220 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002221 issue=int(match.group(1)),
2222 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002223 hostname=parsed_url.netloc,
2224 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002225 return None
2226
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002227 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 """Upload the patch to Rietveld."""
2229 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2230 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2232 if options.emulate_svn_auto_props:
2233 upload_args.append('--emulate_svn_auto_props')
2234
2235 change_desc = None
2236
2237 if options.email is not None:
2238 upload_args.extend(['--email', options.email])
2239
2240 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002241 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 upload_args.extend(['--title', options.title])
2243 if options.message:
2244 upload_args.extend(['--message', options.message])
2245 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002246 print('This branch is associated with issue %s. '
2247 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 else:
nodirca166002016-06-27 10:59:51 -07002249 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002250 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002251 if options.message:
2252 message = options.message
2253 else:
2254 message = CreateDescriptionFromLog(args)
2255 if options.title:
2256 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002258 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002259 change_desc.update_reviewers(options.reviewers, options.tbrs,
2260 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002261 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002262 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002263
2264 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002265 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002266 return 1
2267
2268 upload_args.extend(['--message', change_desc.description])
2269 if change_desc.get_reviewers():
2270 upload_args.append('--reviewers=%s' % ','.join(
2271 change_desc.get_reviewers()))
2272 if options.send_mail:
2273 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002274 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 upload_args.append('--send_mail')
2276
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00002277 # We only skip auto-CC-ing addresses from rietveld.cc when --private or
2278 # --no-autocc is explicitly specified on the command line. Should private
2279 # CL be created due to rietveld.private value, we assume that rietveld.cc
2280 # only contains addresses where private CLs are allowed to be sent.
2281 if options.private or options.no_autocc:
2282 logging.warn('rietveld.cc is ignored since private/no-autocc flag is '
2283 'specified. You need to review and add them manually if '
2284 'necessary.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 cc = self.GetCCListWithoutDefault()
2286 else:
2287 cc = self.GetCCList()
2288 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002289 if change_desc.get_cced():
2290 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 if cc:
2292 upload_args.extend(['--cc', cc])
2293
2294 if options.private or settings.GetDefaultPrivateFlag() == "True":
2295 upload_args.append('--private')
2296
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002297 # Include the upstream repo's URL in the change -- this is useful for
2298 # projects that have their source spread across multiple repos.
2299 remote_url = self.GetGitBaseUrlFromConfig()
2300 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002301 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2302 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2303 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002304 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002306 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002307 if target_ref:
2308 upload_args.extend(['--target_ref', target_ref])
2309
2310 # Look for dependent patchsets. See crbug.com/480453 for more details.
2311 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2312 upstream_branch = ShortBranchName(upstream_branch)
2313 if remote is '.':
2314 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002315 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002316 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002317 print()
2318 print('Skipping dependency patchset upload because git config '
2319 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2320 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002321 else:
2322 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002323 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002324 auth_config=auth_config)
2325 branch_cl_issue_url = branch_cl.GetIssueURL()
2326 branch_cl_issue = branch_cl.GetIssue()
2327 branch_cl_patchset = branch_cl.GetPatchset()
2328 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2329 upload_args.extend(
2330 ['--depends_on_patchset', '%s:%s' % (
2331 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002332 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002333 '\n'
2334 'The current branch (%s) is tracking a local branch (%s) with '
2335 'an associated CL.\n'
2336 'Adding %s/#ps%s as a dependency patchset.\n'
2337 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2338 branch_cl_patchset))
2339
2340 project = settings.GetProject()
2341 if project:
2342 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002343 else:
2344 print()
2345 print('WARNING: Uploading without a project specified. Please ensure '
2346 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2347 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002348
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002349 try:
2350 upload_args = ['upload'] + upload_args + args
2351 logging.info('upload.RealMain(%s)', upload_args)
2352 issue, patchset = upload.RealMain(upload_args)
2353 issue = int(issue)
2354 patchset = int(patchset)
2355 except KeyboardInterrupt:
2356 sys.exit(1)
2357 except:
2358 # If we got an exception after the user typed a description for their
2359 # change, back up the description before re-raising.
2360 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002361 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002362 raise
2363
2364 if not self.GetIssue():
2365 self.SetIssue(issue)
2366 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002367 return 0
2368
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002369
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002370class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002371 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002372 # auth_config is Rietveld thing, kept here to preserve interface only.
2373 super(_GerritChangelistImpl, self).__init__(changelist)
2374 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002375 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002376 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002377 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002378 # Map from change number (issue) to its detail cache.
2379 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002380
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002381 if codereview_host is not None:
2382 assert not codereview_host.startswith('https://'), codereview_host
2383 self._gerrit_host = codereview_host
2384 self._gerrit_server = 'https://%s' % codereview_host
2385
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002386 def _GetGerritHost(self):
2387 # Lazy load of configs.
2388 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002389 if self._gerrit_host and '.' not in self._gerrit_host:
2390 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2391 # This happens for internal stuff http://crbug.com/614312.
2392 parsed = urlparse.urlparse(self.GetRemoteUrl())
2393 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002394 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002395 ' Your current remote is: %s' % self.GetRemoteUrl())
2396 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2397 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002398 return self._gerrit_host
2399
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002400 def _GetGitHost(self):
2401 """Returns git host to be used when uploading change to Gerrit."""
2402 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2403
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404 def GetCodereviewServer(self):
2405 if not self._gerrit_server:
2406 # If we're on a branch then get the server potentially associated
2407 # with that branch.
2408 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002409 self._gerrit_server = self._GitGetBranchConfigValue(
2410 self.CodereviewServerConfigKey())
2411 if self._gerrit_server:
2412 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002413 if not self._gerrit_server:
2414 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2415 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002416 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002417 parts[0] = parts[0] + '-review'
2418 self._gerrit_host = '.'.join(parts)
2419 self._gerrit_server = 'https://%s' % self._gerrit_host
2420 return self._gerrit_server
2421
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002422 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002423 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002424 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002425 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002426 logging.warn('can\'t detect Gerrit project.')
2427 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002428 project = urlparse.urlparse(remote_url).path.strip('/')
2429 if project.endswith('.git'):
2430 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002431 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2432 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2433 # gitiles/git-over-https protocol. E.g.,
2434 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2435 # as
2436 # https://chromium.googlesource.com/v8/v8
2437 if project.startswith('a/'):
2438 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002439 return project
2440
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002441 def _GerritChangeIdentifier(self):
2442 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2443
2444 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002445 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002446 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002447 project = self._GetGerritProject()
2448 if project:
2449 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2450 # Fall back on still unique, but less efficient change number.
2451 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002452
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002453 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002454 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002455 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002456
tandrii5d48c322016-08-18 16:19:37 -07002457 @classmethod
2458 def PatchsetConfigKey(cls):
2459 return 'gerritpatchset'
2460
2461 @classmethod
2462 def CodereviewServerConfigKey(cls):
2463 return 'gerritserver'
2464
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002465 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002466 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002467 if settings.GetGerritSkipEnsureAuthenticated():
2468 # For projects with unusual authentication schemes.
2469 # See http://crbug.com/603378.
2470 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002471
2472 # Check presence of cookies only if using cookies-based auth method.
2473 cookie_auth = gerrit_util.Authenticator.get()
2474 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002475 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002476
2477 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002478 self.GetCodereviewServer()
2479 git_host = self._GetGitHost()
2480 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002481
2482 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2483 git_auth = cookie_auth.get_auth_header(git_host)
2484 if gerrit_auth and git_auth:
2485 if gerrit_auth == git_auth:
2486 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002487 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002488 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002489 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002490 ' %s\n'
2491 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002492 ' Consider running the following command:\n'
2493 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002494 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002495 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002496 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002497 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002498 cookie_auth.get_new_password_message(git_host)))
2499 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002500 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002501 return
2502 else:
2503 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002504 ([] if gerrit_auth else [self._gerrit_host]) +
2505 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002506 DieWithError('Credentials for the following hosts are required:\n'
2507 ' %s\n'
2508 'These are read from %s (or legacy %s)\n'
2509 '%s' % (
2510 '\n '.join(missing),
2511 cookie_auth.get_gitcookies_path(),
2512 cookie_auth.get_netrc_path(),
2513 cookie_auth.get_new_password_message(git_host)))
2514
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002515 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002516 if not self.GetIssue():
2517 return
2518
2519 # Warm change details cache now to avoid RPCs later, reducing latency for
2520 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002521 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002522 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002523
2524 status = self._GetChangeDetail()['status']
2525 if status in ('MERGED', 'ABANDONED'):
2526 DieWithError('Change %s has been %s, new uploads are not allowed' %
2527 (self.GetIssueURL(),
2528 'submitted' if status == 'MERGED' else 'abandoned'))
2529
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002530 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2531 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2532 # Apparently this check is not very important? Otherwise get_auth_email
2533 # could have been added to other implementations of Authenticator.
2534 cookies_auth = gerrit_util.Authenticator.get()
2535 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002536 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002537
2538 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002539 if self.GetIssueOwner() == cookies_user:
2540 return
2541 logging.debug('change %s owner is %s, cookies user is %s',
2542 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002543 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002544 # so ask what Gerrit thinks of this user.
2545 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2546 if details['email'] == self.GetIssueOwner():
2547 return
2548 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002549 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002550 'as %s.\n'
2551 'Uploading may fail due to lack of permissions.' %
2552 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2553 confirm_or_exit(action='upload')
2554
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002555 def _PostUnsetIssueProperties(self):
2556 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002557 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002558
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002559 def GetGerritObjForPresubmit(self):
2560 return presubmit_support.GerritAccessor(self._GetGerritHost())
2561
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002562 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002563 """Apply a rough heuristic to give a simple summary of an issue's review
2564 or CQ status, assuming adherence to a common workflow.
2565
2566 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002567 * 'error' - error from review tool (including deleted issues)
2568 * 'unsent' - no reviewers added
2569 * 'waiting' - waiting for review
2570 * 'reply' - waiting for uploader to reply to review
2571 * 'lgtm' - Code-Review label has been set
2572 * 'commit' - in the commit queue
2573 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002574 """
2575 if not self.GetIssue():
2576 return None
2577
2578 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002579 data = self._GetChangeDetail([
2580 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002581 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002582 return 'error'
2583
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002584 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002585 return 'closed'
2586
Aaron Gable9ab38c62017-04-06 14:36:33 -07002587 if data['labels'].get('Commit-Queue', {}).get('approved'):
2588 # The section will have an "approved" subsection if anyone has voted
2589 # the maximum value on the label.
2590 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002591
Aaron Gable9ab38c62017-04-06 14:36:33 -07002592 if data['labels'].get('Code-Review', {}).get('approved'):
2593 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002594
2595 if not data.get('reviewers', {}).get('REVIEWER', []):
2596 return 'unsent'
2597
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002598 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002599 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2600 last_message_author = messages.pop().get('author', {})
2601 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002602 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2603 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002604 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002605 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002606 if last_message_author.get('_account_id') == owner:
2607 # Most recent message was by owner.
2608 return 'waiting'
2609 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002610 # Some reply from non-owner.
2611 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002612
2613 # Somehow there are no messages even though there are reviewers.
2614 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002615
2616 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002617 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002618 patchset = data['revisions'][data['current_revision']]['_number']
2619 self.SetPatchset(patchset)
2620 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002621
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002622 def FetchDescription(self, force=False):
2623 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2624 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002625 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002626 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002627
dsansomee2d6fd92016-09-08 00:10:47 -07002628 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002629 if gerrit_util.HasPendingChangeEdit(
2630 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002631 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002632 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002633 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002634 'unpublished edit. Either publish the edit in the Gerrit web UI '
2635 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002636
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002637 gerrit_util.DeletePendingChangeEdit(
2638 self._GetGerritHost(), self._GerritChangeIdentifier())
2639 gerrit_util.SetCommitMessage(
2640 self._GetGerritHost(), self._GerritChangeIdentifier(),
2641 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002642
Aaron Gable636b13f2017-07-14 10:42:48 -07002643 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002644 gerrit_util.SetReview(
2645 self._GetGerritHost(), self._GerritChangeIdentifier(),
2646 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002647
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002648 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002649 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002650 messages = self._GetChangeDetail(
2651 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2652 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002653 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002654
2655 # Build dictionary of file comments for easy access and sorting later.
2656 # {author+date: {path: {patchset: {line: url+message}}}}
2657 comments = collections.defaultdict(
2658 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2659 for path, line_comments in file_comments.iteritems():
2660 for comment in line_comments:
2661 if comment.get('tag', '').startswith('autogenerated'):
2662 continue
2663 key = (comment['author']['email'], comment['updated'])
2664 if comment.get('side', 'REVISION') == 'PARENT':
2665 patchset = 'Base'
2666 else:
2667 patchset = 'PS%d' % comment['patch_set']
2668 line = comment.get('line', 0)
2669 url = ('https://%s/c/%s/%s/%s#%s%s' %
2670 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2671 'b' if comment.get('side') == 'PARENT' else '',
2672 str(line) if line else ''))
2673 comments[key][path][patchset][line] = (url, comment['message'])
2674
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002675 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002676 for msg in messages:
2677 # Don't bother showing autogenerated messages.
2678 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2679 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002680 # Gerrit spits out nanoseconds.
2681 assert len(msg['date'].split('.')[-1]) == 9
2682 date = datetime.datetime.strptime(msg['date'][:-3],
2683 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002684 message = msg['message']
2685 key = (msg['author']['email'], msg['date'])
2686 if key in comments:
2687 message += '\n'
2688 for path, patchsets in sorted(comments.get(key, {}).items()):
2689 if readable:
2690 message += '\n%s' % path
2691 for patchset, lines in sorted(patchsets.items()):
2692 for line, (url, content) in sorted(lines.items()):
2693 if line:
2694 line_str = 'Line %d' % line
2695 path_str = '%s:%d:' % (path, line)
2696 else:
2697 line_str = 'File comment'
2698 path_str = '%s:0:' % path
2699 if readable:
2700 message += '\n %s, %s: %s' % (patchset, line_str, url)
2701 message += '\n %s\n' % content
2702 else:
2703 message += '\n%s ' % path_str
2704 message += '\n%s\n' % content
2705
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002706 summary.append(_CommentSummary(
2707 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002708 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002709 sender=msg['author']['email'],
2710 # These could be inferred from the text messages and correlated with
2711 # Code-Review label maximum, however this is not reliable.
2712 # Leaving as is until the need arises.
2713 approval=False,
2714 disapproval=False,
2715 ))
2716 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002717
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002718 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002719 gerrit_util.AbandonChange(
2720 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002721
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002722 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002723 gerrit_util.SubmitChange(
2724 self._GetGerritHost(), self._GerritChangeIdentifier(),
2725 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002727 def _GetChangeDetail(self, options=None, no_cache=False):
2728 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002729
2730 If fresh data is needed, set no_cache=True which will clear cache and
2731 thus new data will be fetched from Gerrit.
2732 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002734 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002735
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002736 # Optimization to avoid multiple RPCs:
2737 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2738 'CURRENT_COMMIT' not in options):
2739 options.append('CURRENT_COMMIT')
2740
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002741 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002742 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002743 options = [o.upper() for o in options]
2744
2745 # Check in cache first unless no_cache is True.
2746 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002747 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002748 else:
2749 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002750 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002751 # Assumption: data fetched before with extra options is suitable
2752 # for return for a smaller set of options.
2753 # For example, if we cached data for
2754 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2755 # and request is for options=[CURRENT_REVISION],
2756 # THEN we can return prior cached data.
2757 if options_set.issubset(cached_options_set):
2758 return data
2759
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002760 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002761 data = gerrit_util.GetChangeDetail(
2762 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002763 except gerrit_util.GerritError as e:
2764 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002765 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002766 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002767
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002768 self._detail_cache.setdefault(cache_key, []).append(
2769 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002770 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002771
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002772 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002773 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002774 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002775 data = gerrit_util.GetChangeCommit(
2776 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002777 except gerrit_util.GerritError as e:
2778 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002779 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002780 raise
agable32978d92016-11-01 12:55:02 -07002781 return data
2782
Olivier Robin75ee7252018-04-13 10:02:56 +02002783 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002784 if git_common.is_dirty_git_tree('land'):
2785 return 1
tandriid60367b2016-06-22 05:25:12 -07002786 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2787 if u'Commit-Queue' in detail.get('labels', {}):
2788 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002789 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2790 'which can test and land changes for you. '
2791 'Are you sure you wish to bypass it?\n',
2792 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002793
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002794 differs = True
tandriic4344b52016-08-29 06:04:54 -07002795 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002796 # Note: git diff outputs nothing if there is no diff.
2797 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002798 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002799 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002800 if detail['current_revision'] == last_upload:
2801 differs = False
2802 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002803 print('WARNING: Local branch contents differ from latest uploaded '
2804 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002805 if differs:
2806 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002807 confirm_or_exit(
2808 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2809 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002810 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002811 elif not bypass_hooks:
2812 hook_results = self.RunHook(
2813 committing=True,
2814 may_prompt=not force,
2815 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002816 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2817 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002818 if not hook_results.should_continue():
2819 return 1
2820
2821 self.SubmitIssue(wait_for_merge=True)
2822 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002823 links = self._GetChangeCommit().get('web_links', [])
2824 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002825 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002826 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002827 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002828 return 0
2829
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002830 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002831 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002832 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002833 assert not directory
2834 assert parsed_issue_arg.valid
2835
2836 self._changelist.issue = parsed_issue_arg.issue
2837
2838 if parsed_issue_arg.hostname:
2839 self._gerrit_host = parsed_issue_arg.hostname
2840 self._gerrit_server = 'https://%s' % self._gerrit_host
2841
tandriic2405f52016-10-10 08:13:15 -07002842 try:
2843 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002844 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002845 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002846
2847 if not parsed_issue_arg.patchset:
2848 # Use current revision by default.
2849 revision_info = detail['revisions'][detail['current_revision']]
2850 patchset = int(revision_info['_number'])
2851 else:
2852 patchset = parsed_issue_arg.patchset
2853 for revision_info in detail['revisions'].itervalues():
2854 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2855 break
2856 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002857 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002858 (parsed_issue_arg.patchset, self.GetIssue()))
2859
Aaron Gable697a91b2018-01-19 15:20:15 -08002860 remote_url = self._changelist.GetRemoteUrl()
2861 if remote_url.endswith('.git'):
2862 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002863 remote_url = remote_url.rstrip('/')
2864
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002865 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002866 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002867
2868 if remote_url != fetch_info['url']:
2869 DieWithError('Trying to patch a change from %s but this repo appears '
2870 'to be %s.' % (fetch_info['url'], remote_url))
2871
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002872 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002873
Aaron Gable62619a32017-06-16 08:22:09 -07002874 if force:
2875 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2876 print('Checked out commit for change %i patchset %i locally' %
2877 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002878 elif nocommit:
2879 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2880 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002881 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002882 RunGit(['cherry-pick', 'FETCH_HEAD'])
2883 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002884 (parsed_issue_arg.issue, patchset))
2885 print('Note: this created a local commit which does not have '
2886 'the same hash as the one uploaded for review. This will make '
2887 'uploading changes based on top of this branch difficult.\n'
2888 'If you want to do that, use "git cl patch --force" instead.')
2889
Stefan Zagerd08043c2017-10-12 12:07:02 -07002890 if self.GetBranch():
2891 self.SetIssue(parsed_issue_arg.issue)
2892 self.SetPatchset(patchset)
2893 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2894 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2895 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2896 else:
2897 print('WARNING: You are in detached HEAD state.\n'
2898 'The patch has been applied to your checkout, but you will not be '
2899 'able to upload a new patch set to the gerrit issue.\n'
2900 'Try using the \'-b\' option if you would like to work on a '
2901 'branch and/or upload a new patch set.')
2902
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002903 return 0
2904
2905 @staticmethod
2906 def ParseIssueURL(parsed_url):
2907 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2908 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002909 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2910 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002911 # Short urls like https://domain/<issue_number> can be used, but don't allow
2912 # specifying the patchset (you'd 404), but we allow that here.
2913 if parsed_url.path == '/':
2914 part = parsed_url.fragment
2915 else:
2916 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002917 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002918 if match:
2919 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002920 issue=int(match.group(3)),
2921 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002922 hostname=parsed_url.netloc,
2923 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002924 return None
2925
tandrii16e0b4e2016-06-07 10:34:28 -07002926 def _GerritCommitMsgHookCheck(self, offer_removal):
2927 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2928 if not os.path.exists(hook):
2929 return
2930 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2931 # custom developer made one.
2932 data = gclient_utils.FileRead(hook)
2933 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2934 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002935 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002936 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002937 'and may interfere with it in subtle ways.\n'
2938 'We recommend you remove the commit-msg hook.')
2939 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002940 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002941 gclient_utils.rm_file_or_tree(hook)
2942 print('Gerrit commit-msg hook removed.')
2943 else:
2944 print('OK, will keep Gerrit commit-msg hook in place.')
2945
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002946 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002947 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002948 if options.squash and options.no_squash:
2949 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002950
2951 if not options.squash and not options.no_squash:
2952 # Load default for user, repo, squash=true, in this order.
2953 options.squash = settings.GetSquashGerritUploads()
2954 elif options.no_squash:
2955 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002956
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002957 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002958 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002959
Aaron Gableb56ad332017-01-06 15:24:31 -08002960 # This may be None; default fallback value is determined in logic below.
2961 title = options.title
2962
Dominic Battre7d1c4842017-10-27 09:17:28 +02002963 # Extract bug number from branch name.
2964 bug = options.bug
2965 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2966 if not bug and match:
2967 bug = match.group(1)
2968
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002969 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002970 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002971 if self.GetIssue():
2972 # Try to get the message from a previous upload.
2973 message = self.GetDescription()
2974 if not message:
2975 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002976 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002977 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002978 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002979 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002980 # When uploading a subsequent patchset, -m|--message is taken
2981 # as the patchset title if --title was not provided.
2982 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002983 else:
2984 default_title = RunGit(
2985 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002986 if options.force:
2987 title = default_title
2988 else:
2989 title = ask_for_data(
2990 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002991 change_id = self._GetChangeDetail()['change_id']
2992 while True:
2993 footer_change_ids = git_footers.get_footer_change_id(message)
2994 if footer_change_ids == [change_id]:
2995 break
2996 if not footer_change_ids:
2997 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002998 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002999 continue
3000 # There is already a valid footer but with different or several ids.
3001 # Doing this automatically is non-trivial as we don't want to lose
3002 # existing other footers, yet we want to append just 1 desired
3003 # Change-Id. Thus, just create a new footer, but let user verify the
3004 # new description.
3005 message = '%s\n\nChange-Id: %s' % (message, change_id)
3006 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08003007 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003008 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08003009 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003010 'Please, check the proposed correction to the description, '
3011 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
3012 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
3013 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003014 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015 if not options.force:
3016 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02003017 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003018 message = change_desc.description
3019 if not message:
3020 DieWithError("Description is empty. Aborting...")
3021 # Continue the while loop.
3022 # Sanity check of this code - we should end up with proper message
3023 # footer.
3024 assert [change_id] == git_footers.get_footer_change_id(message)
3025 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08003026 else: # if not self.GetIssue()
3027 if options.message:
3028 message = options.message
3029 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003030 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08003031 if options.title:
3032 message = options.title + '\n\n' + message
3033 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003034
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003035 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02003036 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08003037 # On first upload, patchset title is always this string, while
3038 # --title flag gets converted to first line of message.
3039 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003040 if not change_desc.description:
3041 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003042 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003043 if len(change_ids) > 1:
3044 DieWithError('too many Change-Id footers, at most 1 allowed.')
3045 if not change_ids:
3046 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003047 change_desc.set_description(git_footers.add_footer_change_id(
3048 change_desc.description,
3049 GenerateGerritChangeId(change_desc.description)))
3050 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003051 assert len(change_ids) == 1
3052 change_id = change_ids[0]
3053
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003054 if options.reviewers or options.tbrs or options.add_owners_to:
3055 change_desc.update_reviewers(options.reviewers, options.tbrs,
3056 options.add_owners_to, change)
3057
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003058 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003059 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3060 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003061 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003062 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3063 desc_tempfile.write(change_desc.description)
3064 desc_tempfile.close()
3065 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3066 '-F', desc_tempfile.name]).strip()
3067 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003068 else:
3069 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003070 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003071 if not change_desc.description:
3072 DieWithError("Description is empty. Aborting...")
3073
3074 if not git_footers.get_footer_change_id(change_desc.description):
3075 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003076 change_desc.set_description(
3077 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003078 if options.reviewers or options.tbrs or options.add_owners_to:
3079 change_desc.update_reviewers(options.reviewers, options.tbrs,
3080 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003081 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003082 # For no-squash mode, we assume the remote called "origin" is the one we
3083 # want. It is not worthwhile to support different workflows for
3084 # no-squash mode.
3085 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003086 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3087
3088 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00003089 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003090 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3091 ref_to_push)]).splitlines()
3092 if len(commits) > 1:
3093 print('WARNING: This will upload %d commits. Run the following command '
3094 'to see which commits will be uploaded: ' % len(commits))
3095 print('git log %s..%s' % (parent, ref_to_push))
3096 print('You can also use `git squash-branch` to squash these into a '
3097 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003098 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003099
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003100 if options.reviewers or options.tbrs or options.add_owners_to:
3101 change_desc.update_reviewers(options.reviewers, options.tbrs,
3102 options.add_owners_to, change)
3103
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00003104 reviewers = sorted(change_desc.get_reviewers())
3105 # Add cc's from the CC_LIST and --cc flag (if any).
3106 if not options.private and not options.no_autocc:
3107 cc = self.GetCCList().split(',')
3108 else:
3109 cc = []
3110 if options.cc:
3111 cc.extend(options.cc)
3112 cc = filter(None, [email.strip() for email in cc])
3113 if change_desc.get_cced():
3114 cc.extend(change_desc.get_cced())
Andrii Shyshkalovba7b0a42018-10-15 03:20:35 +00003115 valid_accounts = gerrit_util.ValidAccounts(
3116 self._GetGerritHost(), reviewers + cc)
3117 logging.debug('accounts %s are valid, %s invalid', sorted(valid_accounts),
3118 set(reviewers + cc).difference(set(valid_accounts)))
3119 # TODO(tandrii): add valid reviwers and ccs to push option.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00003120
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003121 # Extra options that can be specified at push time. Doc:
3122 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003123 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003124
Aaron Gable844cf292017-06-28 11:32:59 -07003125 # By default, new changes are started in WIP mode, and subsequent patchsets
3126 # don't send email. At any time, passing --send-mail will mark the change
3127 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003128 if options.send_mail:
3129 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003130 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04003131 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003132 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003133 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003134 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003135
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003136 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003137 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003138
Aaron Gable9b713dd2016-12-14 16:04:21 -08003139 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003140 # Punctuation and whitespace in |title| must be percent-encoded.
3141 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003142
agablec6787972016-09-09 16:13:34 -07003143 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003144 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003145
rmistry9eadede2016-09-19 11:22:43 -07003146 if options.topic:
3147 # Documentation on Gerrit topics is here:
3148 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003149 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003150
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003151 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003152 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003153 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003154 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003155 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3156
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003157 refspec_suffix = ''
3158 if refspec_opts:
3159 refspec_suffix = '%' + ','.join(refspec_opts)
3160 assert ' ' not in refspec_suffix, (
3161 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3162 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3163
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003164 try:
Edward Lemur83bd7f42018-10-10 00:14:21 +00003165 # TODO(crbug.com/881860): Remove.
Edward Lemur47faa062018-10-11 19:46:02 +00003166 # Clear the log after each git-cl upload run by setting mode='w'.
3167 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
3168 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
3169
3170 GERRIT_ERR_LOGGER.addHandler(handler)
3171 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
3172 # Don't propagate to root logger, so that logs are not printed.
3173 GERRIT_ERR_LOGGER.propagate = 0
3174
Edward Lemur83bd7f42018-10-10 00:14:21 +00003175 # Get interesting headers from git push, to be displayed to the user if
3176 # subsequent Gerrit RPC calls fail.
3177 env = os.environ.copy()
3178 env['GIT_CURL_VERBOSE'] = '1'
3179 class FilterHeaders(object):
3180 """Filter git push headers and store them in a file.
3181
3182 Regular git push output is printed directly.
3183 """
3184
3185 def __init__(self):
3186 # The output from git push that we want to store in a file.
3187 self._output = ''
3188 # Keeps track of whether the current line is part of a request header.
3189 self._on_header = False
3190 # Keeps track of repeated empty lines, which mark the end of a request
3191 # header.
3192 self._last_line_empty = False
3193
3194 def __call__(self, line):
3195 """Handle a single line of git push output."""
3196 if not line:
3197 # Two consecutive empty lines mark the end of a header.
3198 if self._last_line_empty:
3199 self._on_header = False
3200 self._last_line_empty = True
3201 return
3202
3203 self._last_line_empty = False
3204 # A line starting with '>' marks the beggining of a request header.
3205 if line[0] == '>':
3206 self._on_header = True
3207 GERRIT_ERR_LOGGER.info(line)
3208 # Lines not starting with '*' or '<', and not part of a request header
3209 # should be displayed to the user.
3210 elif line[0] not in '*<' and not self._on_header:
3211 print(line)
3212 # Flush after every line: useful for seeing progress when running as
3213 # recipe.
3214 sys.stdout.flush()
3215 # Filter out the cookie and authorization headers.
3216 elif ('cookie: ' not in line.lower()
3217 and 'authorization: ' not in line.lower()):
3218 GERRIT_ERR_LOGGER.info(line)
3219
3220 filter_fn = FilterHeaders()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003221 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003222 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00003223 print_stdout=False,
3224 filter_fn=filter_fn,
3225 env=env)
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003226 except subprocess2.CalledProcessError:
3227 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003228 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003229 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003230 'credential problems:\n'
3231 ' git cl creds-check\n',
3232 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003233
3234 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003235 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003236 change_numbers = [m.group(1)
3237 for m in map(regex.match, push_stdout.splitlines())
3238 if m]
3239 if len(change_numbers) != 1:
3240 DieWithError(
3241 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003242 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003243 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003244 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003245
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003246 if self.GetIssue():
3247 # GetIssue() is not set in case of non-squash uploads according to tests.
3248 # TODO(agable): non-squash uploads in git cl should be removed.
3249 gerrit_util.AddReviewers(
3250 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003251 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003252 reviewers, cc,
3253 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003254
Aaron Gablefd238082017-06-07 13:42:34 -07003255 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003256 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3257 score = 1
3258 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3259 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3260 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003261 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003262 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003263 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003264 msg='Self-approving for TBR',
3265 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003266
Andrii Shyshkalovdd788442018-10-13 17:55:29 +00003267 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3268 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003269 return 0
3270
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003271 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3272 change_desc):
3273 """Computes parent of the generated commit to be uploaded to Gerrit.
3274
3275 Returns revision or a ref name.
3276 """
3277 if custom_cl_base:
3278 # Try to avoid creating additional unintended CLs when uploading, unless
3279 # user wants to take this risk.
3280 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3281 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3282 local_ref_of_target_remote])
3283 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003284 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003285 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3286 'If you proceed with upload, more than 1 CL may be created by '
3287 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3288 'If you are certain that specified base `%s` has already been '
3289 'uploaded to Gerrit as another CL, you may proceed.\n' %
3290 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3291 if not force:
3292 confirm_or_exit(
3293 'Do you take responsibility for cleaning up potential mess '
3294 'resulting from proceeding with upload?',
3295 action='upload')
3296 return custom_cl_base
3297
Aaron Gablef97e33d2017-03-30 15:44:27 -07003298 if remote != '.':
3299 return self.GetCommonAncestorWithUpstream()
3300
3301 # If our upstream branch is local, we base our squashed commit on its
3302 # squashed version.
3303 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3304
Aaron Gablef97e33d2017-03-30 15:44:27 -07003305 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003306 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003307
3308 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003309 # TODO(tandrii): consider checking parent change in Gerrit and using its
3310 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3311 # the tree hash of the parent branch. The upside is less likely bogus
3312 # requests to reupload parent change just because it's uploadhash is
3313 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003314 parent = RunGit(['config',
3315 'branch.%s.gerritsquashhash' % upstream_branch_name],
3316 error_ok=True).strip()
3317 # Verify that the upstream branch has been uploaded too, otherwise
3318 # Gerrit will create additional CLs when uploading.
3319 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3320 RunGitSilent(['rev-parse', parent + ':'])):
3321 DieWithError(
3322 '\nUpload upstream branch %s first.\n'
3323 'It is likely that this branch has been rebased since its last '
3324 'upload, so you just need to upload it again.\n'
3325 '(If you uploaded it with --no-squash, then branch dependencies '
3326 'are not supported, and you should reupload with --squash.)'
3327 % upstream_branch_name,
3328 change_desc)
3329 return parent
3330
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003331 def _AddChangeIdToCommitMessage(self, options, args):
3332 """Re-commits using the current message, assumes the commit hook is in
3333 place.
3334 """
3335 log_desc = options.message or CreateDescriptionFromLog(args)
3336 git_command = ['commit', '--amend', '-m', log_desc]
3337 RunGit(git_command)
3338 new_log_desc = CreateDescriptionFromLog(args)
3339 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003340 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003341 return new_log_desc
3342 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003343 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003344
Ravi Mistry31e7d562018-04-02 12:53:57 -04003345 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3346 """Sets labels on the change based on the provided flags."""
3347 labels = {}
3348 notify = None;
3349 if enable_auto_submit:
3350 labels['Auto-Submit'] = 1
3351 if use_commit_queue:
3352 labels['Commit-Queue'] = 2
3353 elif cq_dry_run:
3354 labels['Commit-Queue'] = 1
3355 notify = False
3356 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003357 gerrit_util.SetReview(
3358 self._GetGerritHost(),
3359 self._GerritChangeIdentifier(),
3360 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003361
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003362 def SetCQState(self, new_state):
3363 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003364 vote_map = {
3365 _CQState.NONE: 0,
3366 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003367 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003368 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003369 labels = {'Commit-Queue': vote_map[new_state]}
3370 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003371 gerrit_util.SetReview(
3372 self._GetGerritHost(), self._GerritChangeIdentifier(),
3373 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003374
tandriie113dfd2016-10-11 10:20:12 -07003375 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003376 try:
3377 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003378 except GerritChangeNotExists:
3379 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003380
3381 if data['status'] in ('ABANDONED', 'MERGED'):
3382 return 'CL %s is closed' % self.GetIssue()
3383
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003384 def GetTryJobProperties(self, patchset=None):
3385 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003386 data = self._GetChangeDetail(['ALL_REVISIONS'])
3387 patchset = int(patchset or self.GetPatchset())
3388 assert patchset
3389 revision_data = None # Pylint wants it to be defined.
3390 for revision_data in data['revisions'].itervalues():
3391 if int(revision_data['_number']) == patchset:
3392 break
3393 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003394 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003395 (patchset, self.GetIssue()))
3396 return {
3397 'patch_issue': self.GetIssue(),
3398 'patch_set': patchset or self.GetPatchset(),
3399 'patch_project': data['project'],
3400 'patch_storage': 'gerrit',
3401 'patch_ref': revision_data['fetch']['http']['ref'],
3402 'patch_repository_url': revision_data['fetch']['http']['url'],
3403 'patch_gerrit_url': self.GetCodereviewServer(),
3404 }
tandriie113dfd2016-10-11 10:20:12 -07003405
tandriide281ae2016-10-12 06:02:30 -07003406 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003407 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003408
Edward Lemur707d70b2018-02-07 00:50:14 +01003409 def GetReviewers(self):
3410 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3411 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3412
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003413
3414_CODEREVIEW_IMPLEMENTATIONS = {
3415 'rietveld': _RietveldChangelistImpl,
3416 'gerrit': _GerritChangelistImpl,
3417}
3418
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003419
iannuccie53c9352016-08-17 14:40:40 -07003420def _add_codereview_issue_select_options(parser, extra=""):
3421 _add_codereview_select_options(parser)
3422
3423 text = ('Operate on this issue number instead of the current branch\'s '
3424 'implicit issue.')
3425 if extra:
3426 text += ' '+extra
3427 parser.add_option('-i', '--issue', type=int, help=text)
3428
3429
3430def _process_codereview_issue_select_options(parser, options):
3431 _process_codereview_select_options(parser, options)
3432 if options.issue is not None and not options.forced_codereview:
3433 parser.error('--issue must be specified with either --rietveld or --gerrit')
3434
3435
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003436def _add_codereview_select_options(parser):
3437 """Appends --gerrit and --rietveld options to force specific codereview."""
3438 parser.codereview_group = optparse.OptionGroup(
3439 parser, 'EXPERIMENTAL! Codereview override options')
3440 parser.add_option_group(parser.codereview_group)
3441 parser.codereview_group.add_option(
3442 '--gerrit', action='store_true',
3443 help='Force the use of Gerrit for codereview')
3444 parser.codereview_group.add_option(
3445 '--rietveld', action='store_true',
3446 help='Force the use of Rietveld for codereview')
3447
3448
3449def _process_codereview_select_options(parser, options):
3450 if options.gerrit and options.rietveld:
3451 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3452 options.forced_codereview = None
3453 if options.gerrit:
3454 options.forced_codereview = 'gerrit'
3455 elif options.rietveld:
3456 options.forced_codereview = 'rietveld'
3457
3458
tandriif9aefb72016-07-01 09:06:51 -07003459def _get_bug_line_values(default_project, bugs):
3460 """Given default_project and comma separated list of bugs, yields bug line
3461 values.
3462
3463 Each bug can be either:
3464 * a number, which is combined with default_project
3465 * string, which is left as is.
3466
3467 This function may produce more than one line, because bugdroid expects one
3468 project per line.
3469
3470 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3471 ['v8:123', 'chromium:789']
3472 """
3473 default_bugs = []
3474 others = []
3475 for bug in bugs.split(','):
3476 bug = bug.strip()
3477 if bug:
3478 try:
3479 default_bugs.append(int(bug))
3480 except ValueError:
3481 others.append(bug)
3482
3483 if default_bugs:
3484 default_bugs = ','.join(map(str, default_bugs))
3485 if default_project:
3486 yield '%s:%s' % (default_project, default_bugs)
3487 else:
3488 yield default_bugs
3489 for other in sorted(others):
3490 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3491 yield other
3492
3493
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003494class ChangeDescription(object):
3495 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003496 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003497 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003498 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003499 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003500 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3501 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3502 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3503 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003504
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003505 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003506 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003507
agable@chromium.org42c20792013-09-12 17:34:49 +00003508 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003509 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003510 return '\n'.join(self._description_lines)
3511
3512 def set_description(self, desc):
3513 if isinstance(desc, basestring):
3514 lines = desc.splitlines()
3515 else:
3516 lines = [line.rstrip() for line in desc]
3517 while lines and not lines[0]:
3518 lines.pop(0)
3519 while lines and not lines[-1]:
3520 lines.pop(-1)
3521 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003522
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003523 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3524 """Rewrites the R=/TBR= line(s) as a single line each.
3525
3526 Args:
3527 reviewers (list(str)) - list of additional emails to use for reviewers.
3528 tbrs (list(str)) - list of additional emails to use for TBRs.
3529 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3530 the change that are missing OWNER coverage. If this is not None, you
3531 must also pass a value for `change`.
3532 change (Change) - The Change that should be used for OWNERS lookups.
3533 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003534 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003535 assert isinstance(tbrs, list), tbrs
3536
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003537 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003538 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003539
3540 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003541 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003542
3543 reviewers = set(reviewers)
3544 tbrs = set(tbrs)
3545 LOOKUP = {
3546 'TBR': tbrs,
3547 'R': reviewers,
3548 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003549
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003550 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003551 regexp = re.compile(self.R_LINE)
3552 matches = [regexp.match(line) for line in self._description_lines]
3553 new_desc = [l for i, l in enumerate(self._description_lines)
3554 if not matches[i]]
3555 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003556
agable@chromium.org42c20792013-09-12 17:34:49 +00003557 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003558
3559 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003560 for match in matches:
3561 if not match:
3562 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003563 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3564
3565 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003566 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003567 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003568 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003569 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003570 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003571 LOOKUP[add_owners_to].update(
3572 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003573
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003574 # If any folks ended up in both groups, remove them from tbrs.
3575 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003576
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003577 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3578 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003579
3580 # Put the new lines in the description where the old first R= line was.
3581 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3582 if 0 <= line_loc < len(self._description_lines):
3583 if new_tbr_line:
3584 self._description_lines.insert(line_loc, new_tbr_line)
3585 if new_r_line:
3586 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003587 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003588 if new_r_line:
3589 self.append_footer(new_r_line)
3590 if new_tbr_line:
3591 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003592
Aaron Gable3a16ed12017-03-23 10:51:55 -07003593 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003594 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003595 self.set_description([
3596 '# Enter a description of the change.',
3597 '# This will be displayed on the codereview site.',
3598 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003599 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003600 '--------------------',
3601 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003602
agable@chromium.org42c20792013-09-12 17:34:49 +00003603 regexp = re.compile(self.BUG_LINE)
3604 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003605 prefix = settings.GetBugPrefix()
3606 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003607 if git_footer:
3608 self.append_footer('Bug: %s' % ', '.join(values))
3609 else:
3610 for value in values:
3611 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003612
agable@chromium.org42c20792013-09-12 17:34:49 +00003613 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003614 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003615 if not content:
3616 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003617 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003618
Bruce Dawson2377b012018-01-11 16:46:49 -08003619 # Strip off comments and default inserted "Bug:" line.
3620 clean_lines = [line.rstrip() for line in lines if not
3621 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003622 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003623 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003624 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003625
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003626 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003627 """Adds a footer line to the description.
3628
3629 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3630 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3631 that Gerrit footers are always at the end.
3632 """
3633 parsed_footer_line = git_footers.parse_footer(line)
3634 if parsed_footer_line:
3635 # Line is a gerrit footer in the form: Footer-Key: any value.
3636 # Thus, must be appended observing Gerrit footer rules.
3637 self.set_description(
3638 git_footers.add_footer(self.description,
3639 key=parsed_footer_line[0],
3640 value=parsed_footer_line[1]))
3641 return
3642
3643 if not self._description_lines:
3644 self._description_lines.append(line)
3645 return
3646
3647 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3648 if gerrit_footers:
3649 # git_footers.split_footers ensures that there is an empty line before
3650 # actual (gerrit) footers, if any. We have to keep it that way.
3651 assert top_lines and top_lines[-1] == ''
3652 top_lines, separator = top_lines[:-1], top_lines[-1:]
3653 else:
3654 separator = [] # No need for separator if there are no gerrit_footers.
3655
3656 prev_line = top_lines[-1] if top_lines else ''
3657 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3658 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3659 top_lines.append('')
3660 top_lines.append(line)
3661 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003662
tandrii99a72f22016-08-17 14:33:24 -07003663 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003664 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003665 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003666 reviewers = [match.group(2).strip()
3667 for match in matches
3668 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003669 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003670
bradnelsond975b302016-10-23 12:20:23 -07003671 def get_cced(self):
3672 """Retrieves the list of reviewers."""
3673 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3674 cced = [match.group(2).strip() for match in matches if match]
3675 return cleanup_list(cced)
3676
Nodir Turakulov23b82142017-11-16 11:04:25 -08003677 def get_hash_tags(self):
3678 """Extracts and sanitizes a list of Gerrit hashtags."""
3679 subject = (self._description_lines or ('',))[0]
3680 subject = re.sub(
3681 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3682
3683 tags = []
3684 start = 0
3685 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3686 while True:
3687 m = bracket_exp.match(subject, start)
3688 if not m:
3689 break
3690 tags.append(self.sanitize_hash_tag(m.group(1)))
3691 start = m.end()
3692
3693 if not tags:
3694 # Try "Tag: " prefix.
3695 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3696 if m:
3697 tags.append(self.sanitize_hash_tag(m.group(1)))
3698 return tags
3699
3700 @classmethod
3701 def sanitize_hash_tag(cls, tag):
3702 """Returns a sanitized Gerrit hash tag.
3703
3704 A sanitized hashtag can be used as a git push refspec parameter value.
3705 """
3706 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3707
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003708 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3709 """Updates this commit description given the parent.
3710
3711 This is essentially what Gnumbd used to do.
3712 Consult https://goo.gl/WMmpDe for more details.
3713 """
3714 assert parent_msg # No, orphan branch creation isn't supported.
3715 assert parent_hash
3716 assert dest_ref
3717 parent_footer_map = git_footers.parse_footers(parent_msg)
3718 # This will also happily parse svn-position, which GnumbD is no longer
3719 # supporting. While we'd generate correct footers, the verifier plugin
3720 # installed in Gerrit will block such commit (ie git push below will fail).
3721 parent_position = git_footers.get_position(parent_footer_map)
3722
3723 # Cherry-picks may have last line obscuring their prior footers,
3724 # from git_footers perspective. This is also what Gnumbd did.
3725 cp_line = None
3726 if (self._description_lines and
3727 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3728 cp_line = self._description_lines.pop()
3729
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003730 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003731
3732 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3733 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003734 for i, line in enumerate(footer_lines):
3735 k, v = git_footers.parse_footer(line) or (None, None)
3736 if k and k.startswith('Cr-'):
3737 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003738
3739 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003740 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003741 if parent_position[0] == dest_ref:
3742 # Same branch as parent.
3743 number = int(parent_position[1]) + 1
3744 else:
3745 number = 1 # New branch, and extra lineage.
3746 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3747 int(parent_position[1])))
3748
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003749 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3750 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003751
3752 self._description_lines = top_lines
3753 if cp_line:
3754 self._description_lines.append(cp_line)
3755 if self._description_lines[-1] != '':
3756 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003757 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003758
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003759
Aaron Gablea1bab272017-04-11 16:38:18 -07003760def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003761 """Retrieves the reviewers that approved a CL from the issue properties with
3762 messages.
3763
3764 Note that the list may contain reviewers that are not committer, thus are not
3765 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003766
3767 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003768 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003769 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003770 return sorted(
3771 set(
3772 message['sender']
3773 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003774 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003775 )
3776 )
3777
3778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003779def FindCodereviewSettingsFile(filename='codereview.settings'):
3780 """Finds the given file starting in the cwd and going up.
3781
3782 Only looks up to the top of the repository unless an
3783 'inherit-review-settings-ok' file exists in the root of the repository.
3784 """
3785 inherit_ok_file = 'inherit-review-settings-ok'
3786 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003787 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003788 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3789 root = '/'
3790 while True:
3791 if filename in os.listdir(cwd):
3792 if os.path.isfile(os.path.join(cwd, filename)):
3793 return open(os.path.join(cwd, filename))
3794 if cwd == root:
3795 break
3796 cwd = os.path.dirname(cwd)
3797
3798
3799def LoadCodereviewSettingsFromFile(fileobj):
3800 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003801 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803 def SetProperty(name, setting, unset_error_ok=False):
3804 fullname = 'rietveld.' + name
3805 if setting in keyvals:
3806 RunGit(['config', fullname, keyvals[setting]])
3807 else:
3808 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3809
tandrii48df5812016-10-17 03:55:37 -07003810 if not keyvals.get('GERRIT_HOST', False):
3811 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003812 # Only server setting is required. Other settings can be absent.
3813 # In that case, we ignore errors raised during option deletion attempt.
3814 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003815 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3817 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003818 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003819 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3820 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003821 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003822 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3823 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003825 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003826 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003827
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003828 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003829 RunGit(['config', 'gerrit.squash-uploads',
3830 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003831
tandrii@chromium.org28253532016-04-14 13:46:56 +00003832 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003833 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003834 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003836 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003837 # should be of the form
3838 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3839 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3841 keyvals['ORIGIN_URL_CONFIG']])
3842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003843
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003844def urlretrieve(source, destination):
3845 """urllib is broken for SSL connections via a proxy therefore we
3846 can't use urllib.urlretrieve()."""
3847 with open(destination, 'w') as f:
3848 f.write(urllib2.urlopen(source).read())
3849
3850
ukai@chromium.org712d6102013-11-27 00:52:58 +00003851def hasSheBang(fname):
3852 """Checks fname is a #! script."""
3853 with open(fname) as f:
3854 return f.read(2).startswith('#!')
3855
3856
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003857# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3858def DownloadHooks(*args, **kwargs):
3859 pass
3860
3861
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003862def DownloadGerritHook(force):
3863 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003864
3865 Args:
3866 force: True to update hooks. False to install hooks if not present.
3867 """
3868 if not settings.GetIsGerrit():
3869 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003870 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003871 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3872 if not os.access(dst, os.X_OK):
3873 if os.path.exists(dst):
3874 if not force:
3875 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003876 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003877 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003878 if not hasSheBang(dst):
3879 DieWithError('Not a script: %s\n'
3880 'You need to download from\n%s\n'
3881 'into .git/hooks/commit-msg and '
3882 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003883 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3884 except Exception:
3885 if os.path.exists(dst):
3886 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003887 DieWithError('\nFailed to download hooks.\n'
3888 'You need to download from\n%s\n'
3889 'into .git/hooks/commit-msg and '
3890 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003891
3892
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003893def GetRietveldCodereviewSettingsInteractively():
3894 """Prompt the user for settings."""
3895 server = settings.GetDefaultServerUrl(error_ok=True)
3896 prompt = 'Rietveld server (host[:port])'
3897 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3898 newserver = ask_for_data(prompt + ':')
3899 if not server and not newserver:
3900 newserver = DEFAULT_SERVER
3901 if newserver:
3902 newserver = gclient_utils.UpgradeToHttps(newserver)
3903 if newserver != server:
3904 RunGit(['config', 'rietveld.server', newserver])
3905
3906 def SetProperty(initial, caption, name, is_url):
3907 prompt = caption
3908 if initial:
3909 prompt += ' ("x" to clear) [%s]' % initial
3910 new_val = ask_for_data(prompt + ':')
3911 if new_val == 'x':
3912 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3913 elif new_val:
3914 if is_url:
3915 new_val = gclient_utils.UpgradeToHttps(new_val)
3916 if new_val != initial:
3917 RunGit(['config', 'rietveld.' + name, new_val])
3918
3919 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3920 SetProperty(settings.GetDefaultPrivateFlag(),
3921 'Private flag (rietveld only)', 'private', False)
3922 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3923 'tree-status-url', False)
3924 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3925 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3926 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3927 'run-post-upload-hook', False)
3928
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003929
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003930class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003931 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003932
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003933 _GOOGLESOURCE = 'googlesource.com'
3934
3935 def __init__(self):
3936 # Cached list of [host, identity, source], where source is either
3937 # .gitcookies or .netrc.
3938 self._all_hosts = None
3939
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003940 def ensure_configured_gitcookies(self):
3941 """Runs checks and suggests fixes to make git use .gitcookies from default
3942 path."""
3943 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3944 configured_path = RunGitSilent(
3945 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003946 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003947 if configured_path:
3948 self._ensure_default_gitcookies_path(configured_path, default)
3949 else:
3950 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003951
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003952 @staticmethod
3953 def _ensure_default_gitcookies_path(configured_path, default_path):
3954 assert configured_path
3955 if configured_path == default_path:
3956 print('git is already configured to use your .gitcookies from %s' %
3957 configured_path)
3958 return
3959
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003960 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003961 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3962 (configured_path, default_path))
3963
3964 if not os.path.exists(configured_path):
3965 print('However, your configured .gitcookies file is missing.')
3966 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3967 action='reconfigure')
3968 RunGit(['config', '--global', 'http.cookiefile', default_path])
3969 return
3970
3971 if os.path.exists(default_path):
3972 print('WARNING: default .gitcookies file already exists %s' %
3973 default_path)
3974 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3975 default_path)
3976
3977 confirm_or_exit('Move existing .gitcookies to default location?',
3978 action='move')
3979 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003980 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003981 print('Moved and reconfigured git to use .gitcookies from %s' %
3982 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003983
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003984 @staticmethod
3985 def _configure_gitcookies_path(default_path):
3986 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3987 if os.path.exists(netrc_path):
3988 print('You seem to be using outdated .netrc for git credentials: %s' %
3989 netrc_path)
3990 print('This tool will guide you through setting up recommended '
3991 '.gitcookies store for git credentials.\n'
3992 '\n'
3993 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3994 ' git config --global --unset http.cookiefile\n'
3995 ' mv %s %s.backup\n\n' % (default_path, default_path))
3996 confirm_or_exit(action='setup .gitcookies')
3997 RunGit(['config', '--global', 'http.cookiefile', default_path])
3998 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003999
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004000 def get_hosts_with_creds(self, include_netrc=False):
4001 if self._all_hosts is None:
4002 a = gerrit_util.CookiesAuthenticator()
4003 self._all_hosts = [
4004 (h, u, s)
4005 for h, u, s in itertools.chain(
4006 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
4007 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
4008 )
4009 if h.endswith(self._GOOGLESOURCE)
4010 ]
4011
4012 if include_netrc:
4013 return self._all_hosts
4014 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
4015
4016 def print_current_creds(self, include_netrc=False):
4017 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
4018 if not hosts:
4019 print('No Git/Gerrit credentials found')
4020 return
4021 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
4022 header = [('Host', 'User', 'Which file'),
4023 ['=' * l for l in lengths]]
4024 for row in (header + hosts):
4025 print('\t'.join((('%%+%ds' % l) % s)
4026 for l, s in zip(lengths, row)))
4027
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004028 @staticmethod
4029 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08004030 """Parses identity "git-<username>.domain" into <username> and domain."""
4031 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02004032 # distinguishable from sub-domains. But we do know typical domains:
4033 if identity.endswith('.chromium.org'):
4034 domain = 'chromium.org'
4035 username = identity[:-len('.chromium.org')]
4036 else:
4037 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004038 if username.startswith('git-'):
4039 username = username[len('git-'):]
4040 return username, domain
4041
4042 def _get_usernames_of_domain(self, domain):
4043 """Returns list of usernames referenced by .gitcookies in a given domain."""
4044 identities_by_domain = {}
4045 for _, identity, _ in self.get_hosts_with_creds():
4046 username, domain = self._parse_identity(identity)
4047 identities_by_domain.setdefault(domain, []).append(username)
4048 return identities_by_domain.get(domain)
4049
4050 def _canonical_git_googlesource_host(self, host):
4051 """Normalizes Gerrit hosts (with '-review') to Git host."""
4052 assert host.endswith(self._GOOGLESOURCE)
4053 # Prefix doesn't include '.' at the end.
4054 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
4055 if prefix.endswith('-review'):
4056 prefix = prefix[:-len('-review')]
4057 return prefix + '.' + self._GOOGLESOURCE
4058
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004059 def _canonical_gerrit_googlesource_host(self, host):
4060 git_host = self._canonical_git_googlesource_host(host)
4061 prefix = git_host.split('.', 1)[0]
4062 return prefix + '-review.' + self._GOOGLESOURCE
4063
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004064 def _get_counterpart_host(self, host):
4065 assert host.endswith(self._GOOGLESOURCE)
4066 git = self._canonical_git_googlesource_host(host)
4067 gerrit = self._canonical_gerrit_googlesource_host(git)
4068 return git if gerrit == host else gerrit
4069
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004070 def has_generic_host(self):
4071 """Returns whether generic .googlesource.com has been configured.
4072
4073 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
4074 """
4075 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
4076 if host == '.' + self._GOOGLESOURCE:
4077 return True
4078 return False
4079
4080 def _get_git_gerrit_identity_pairs(self):
4081 """Returns map from canonic host to pair of identities (Git, Gerrit).
4082
4083 One of identities might be None, meaning not configured.
4084 """
4085 host_to_identity_pairs = {}
4086 for host, identity, _ in self.get_hosts_with_creds():
4087 canonical = self._canonical_git_googlesource_host(host)
4088 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
4089 idx = 0 if canonical == host else 1
4090 pair[idx] = identity
4091 return host_to_identity_pairs
4092
4093 def get_partially_configured_hosts(self):
4094 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004095 (host if i1 else self._canonical_gerrit_googlesource_host(host))
4096 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
4097 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004098
4099 def get_conflicting_hosts(self):
4100 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004101 host
4102 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004103 if None not in (i1, i2) and i1 != i2)
4104
4105 def get_duplicated_hosts(self):
4106 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
4107 return set(host for host, count in counters.iteritems() if count > 1)
4108
4109 _EXPECTED_HOST_IDENTITY_DOMAINS = {
4110 'chromium.googlesource.com': 'chromium.org',
4111 'chrome-internal.googlesource.com': 'google.com',
4112 }
4113
4114 def get_hosts_with_wrong_identities(self):
4115 """Finds hosts which **likely** reference wrong identities.
4116
4117 Note: skips hosts which have conflicting identities for Git and Gerrit.
4118 """
4119 hosts = set()
4120 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4121 pair = self._get_git_gerrit_identity_pairs().get(host)
4122 if pair and pair[0] == pair[1]:
4123 _, domain = self._parse_identity(pair[0])
4124 if domain != expected:
4125 hosts.add(host)
4126 return hosts
4127
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004128 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004129 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004130 hosts = sorted(hosts)
4131 assert hosts
4132 if extra_column_func is None:
4133 extras = [''] * len(hosts)
4134 else:
4135 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004136 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4137 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004138 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004139 lines.append(tmpl % he)
4140 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004141
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004142 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004143 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004144 yield ('.googlesource.com wildcard record detected',
4145 ['Chrome Infrastructure team recommends to list full host names '
4146 'explicitly.'],
4147 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004148
4149 dups = self.get_duplicated_hosts()
4150 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004151 yield ('The following hosts were defined twice',
4152 self._format_hosts(dups),
4153 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004154
4155 partial = self.get_partially_configured_hosts()
4156 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004157 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4158 'These hosts are missing',
4159 self._format_hosts(partial, lambda host: 'but %s defined' %
4160 self._get_counterpart_host(host)),
4161 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004162
4163 conflicting = self.get_conflicting_hosts()
4164 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004165 yield ('The following Git hosts have differing credentials from their '
4166 'Gerrit counterparts',
4167 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4168 tuple(self._get_git_gerrit_identity_pairs()[host])),
4169 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004170
4171 wrong = self.get_hosts_with_wrong_identities()
4172 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004173 yield ('These hosts likely use wrong identity',
4174 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4175 (self._get_git_gerrit_identity_pairs()[host][0],
4176 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4177 wrong)
4178
4179 def find_and_report_problems(self):
4180 """Returns True if there was at least one problem, else False."""
4181 found = False
4182 bad_hosts = set()
4183 for title, sublines, hosts in self._find_problems():
4184 if not found:
4185 found = True
4186 print('\n\n.gitcookies problem report:\n')
4187 bad_hosts.update(hosts or [])
4188 print(' %s%s' % (title , (':' if sublines else '')))
4189 if sublines:
4190 print()
4191 print(' %s' % '\n '.join(sublines))
4192 print()
4193
4194 if bad_hosts:
4195 assert found
4196 print(' You can manually remove corresponding lines in your %s file and '
4197 'visit the following URLs with correct account to generate '
4198 'correct credential lines:\n' %
4199 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4200 print(' %s' % '\n '.join(sorted(set(
4201 gerrit_util.CookiesAuthenticator().get_new_password_url(
4202 self._canonical_git_googlesource_host(host))
4203 for host in bad_hosts
4204 ))))
4205 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004206
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004207
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004208@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004209def CMDcreds_check(parser, args):
4210 """Checks credentials and suggests changes."""
4211 _, _ = parser.parse_args(args)
4212
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004213 # Code below checks .gitcookies. Abort if using something else.
4214 authn = gerrit_util.Authenticator.get()
4215 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4216 if isinstance(authn, gerrit_util.GceAuthenticator):
4217 DieWithError(
4218 'This command is not designed for GCE, are you on a bot?\n'
4219 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4220 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004221 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004222 'This command is not designed for bot environment. It checks '
4223 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004224
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004225 checker = _GitCookiesChecker()
4226 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004227
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004228 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004229 checker.print_current_creds(include_netrc=True)
4230
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004231 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004232 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004233 return 0
4234 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004235
4236
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004237@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004238@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004239def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004240 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004241
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004242 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004243 # TODO(tandrii): remove this once we switch to Gerrit.
4244 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004245 parser.add_option('--activate-update', action='store_true',
4246 help='activate auto-updating [rietveld] section in '
4247 '.git/config')
4248 parser.add_option('--deactivate-update', action='store_true',
4249 help='deactivate auto-updating [rietveld] section in '
4250 '.git/config')
4251 options, args = parser.parse_args(args)
4252
4253 if options.deactivate_update:
4254 RunGit(['config', 'rietveld.autoupdate', 'false'])
4255 return
4256
4257 if options.activate_update:
4258 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4259 return
4260
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004261 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004262 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004263 return 0
4264
4265 url = args[0]
4266 if not url.endswith('codereview.settings'):
4267 url = os.path.join(url, 'codereview.settings')
4268
4269 # Load code review settings and download hooks (if available).
4270 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4271 return 0
4272
4273
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004274@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004275def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004276 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004277 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4278 branch = ShortBranchName(branchref)
4279 _, args = parser.parse_args(args)
4280 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004282 return RunGit(['config', 'branch.%s.base-url' % branch],
4283 error_ok=False).strip()
4284 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004285 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004286 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4287 error_ok=False).strip()
4288
4289
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004290def color_for_status(status):
4291 """Maps a Changelist status to color, for CMDstatus and other tools."""
4292 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004293 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004294 'waiting': Fore.BLUE,
4295 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004296 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004297 'lgtm': Fore.GREEN,
4298 'commit': Fore.MAGENTA,
4299 'closed': Fore.CYAN,
4300 'error': Fore.WHITE,
4301 }.get(status, Fore.WHITE)
4302
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004303
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004304def get_cl_statuses(changes, fine_grained, max_processes=None):
4305 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004306
4307 If fine_grained is true, this will fetch CL statuses from the server.
4308 Otherwise, simply indicate if there's a matching url for the given branches.
4309
4310 If max_processes is specified, it is used as the maximum number of processes
4311 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4312 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004313
4314 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004315 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004316 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004317 upload.verbosity = 0
4318
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004319 if not changes:
4320 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004321
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004322 if not fine_grained:
4323 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004324 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004325 for cl in changes:
4326 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004327 return
4328
4329 # First, sort out authentication issues.
4330 logging.debug('ensuring credentials exist')
4331 for cl in changes:
4332 cl.EnsureAuthenticated(force=False, refresh=True)
4333
4334 def fetch(cl):
4335 try:
4336 return (cl, cl.GetStatus())
4337 except:
4338 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004339 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004340 raise
4341
4342 threads_count = len(changes)
4343 if max_processes:
4344 threads_count = max(1, min(threads_count, max_processes))
4345 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4346
4347 pool = ThreadPool(threads_count)
4348 fetched_cls = set()
4349 try:
4350 it = pool.imap_unordered(fetch, changes).__iter__()
4351 while True:
4352 try:
4353 cl, status = it.next(timeout=5)
4354 except multiprocessing.TimeoutError:
4355 break
4356 fetched_cls.add(cl)
4357 yield cl, status
4358 finally:
4359 pool.close()
4360
4361 # Add any branches that failed to fetch.
4362 for cl in set(changes) - fetched_cls:
4363 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004364
rmistry@google.com2dd99862015-06-22 12:22:18 +00004365
4366def upload_branch_deps(cl, args):
4367 """Uploads CLs of local branches that are dependents of the current branch.
4368
4369 If the local branch dependency tree looks like:
4370 test1 -> test2.1 -> test3.1
4371 -> test3.2
4372 -> test2.2 -> test3.3
4373
4374 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4375 run on the dependent branches in this order:
4376 test2.1, test3.1, test3.2, test2.2, test3.3
4377
4378 Note: This function does not rebase your local dependent branches. Use it when
4379 you make a change to the parent branch that will not conflict with its
4380 dependent branches, and you would like their dependencies updated in
4381 Rietveld.
4382 """
4383 if git_common.is_dirty_git_tree('upload-branch-deps'):
4384 return 1
4385
4386 root_branch = cl.GetBranch()
4387 if root_branch is None:
4388 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4389 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004390 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004391 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4392 'patchset dependencies without an uploaded CL.')
4393
4394 branches = RunGit(['for-each-ref',
4395 '--format=%(refname:short) %(upstream:short)',
4396 'refs/heads'])
4397 if not branches:
4398 print('No local branches found.')
4399 return 0
4400
4401 # Create a dictionary of all local branches to the branches that are dependent
4402 # on it.
4403 tracked_to_dependents = collections.defaultdict(list)
4404 for b in branches.splitlines():
4405 tokens = b.split()
4406 if len(tokens) == 2:
4407 branch_name, tracked = tokens
4408 tracked_to_dependents[tracked].append(branch_name)
4409
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print()
4411 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004412 dependents = []
4413 def traverse_dependents_preorder(branch, padding=''):
4414 dependents_to_process = tracked_to_dependents.get(branch, [])
4415 padding += ' '
4416 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004417 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004418 dependents.append(dependent)
4419 traverse_dependents_preorder(dependent, padding)
4420 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004421 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004422
4423 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004424 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004425 return 0
4426
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004427 confirm_or_exit('This command will checkout all dependent branches and run '
4428 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004429
andybons@chromium.org962f9462016-02-03 20:00:42 +00004430 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004431 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004432 args.extend(['-t', 'Updated patchset dependency'])
4433
rmistry@google.com2dd99862015-06-22 12:22:18 +00004434 # Record all dependents that failed to upload.
4435 failures = {}
4436 # Go through all dependents, checkout the branch and upload.
4437 try:
4438 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004439 print()
4440 print('--------------------------------------')
4441 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004442 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004443 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004444 try:
4445 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004446 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004447 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004448 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004449 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004451 finally:
4452 # Swap back to the original root branch.
4453 RunGit(['checkout', '-q', root_branch])
4454
vapiera7fbd5a2016-06-16 09:17:49 -07004455 print()
4456 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004457 for dependent_branch in dependents:
4458 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print(' %s : %s' % (dependent_branch, upload_status))
4460 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004461
4462 return 0
4463
4464
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004465@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004466def CMDarchive(parser, args):
4467 """Archives and deletes branches associated with closed changelists."""
4468 parser.add_option(
4469 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004470 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004471 parser.add_option(
4472 '-f', '--force', action='store_true',
4473 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004474 parser.add_option(
4475 '-d', '--dry-run', action='store_true',
4476 help='Skip the branch tagging and removal steps.')
4477 parser.add_option(
4478 '-t', '--notags', action='store_true',
4479 help='Do not tag archived branches. '
4480 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004481
4482 auth.add_auth_options(parser)
4483 options, args = parser.parse_args(args)
4484 if args:
4485 parser.error('Unsupported args: %s' % ' '.join(args))
4486 auth_config = auth.extract_auth_config_from_options(options)
4487
4488 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4489 if not branches:
4490 return 0
4491
vapiera7fbd5a2016-06-16 09:17:49 -07004492 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004493 changes = [Changelist(branchref=b, auth_config=auth_config)
4494 for b in branches.splitlines()]
4495 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4496 statuses = get_cl_statuses(changes,
4497 fine_grained=True,
4498 max_processes=options.maxjobs)
4499 proposal = [(cl.GetBranch(),
4500 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4501 for cl, status in statuses
4502 if status == 'closed']
4503 proposal.sort()
4504
4505 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004506 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004507 return 0
4508
4509 current_branch = GetCurrentBranch()
4510
vapiera7fbd5a2016-06-16 09:17:49 -07004511 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004512 if options.notags:
4513 for next_item in proposal:
4514 print(' ' + next_item[0])
4515 else:
4516 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4517 for next_item in proposal:
4518 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004519
kmarshall9249e012016-08-23 12:02:16 -07004520 # Quit now on precondition failure or if instructed by the user, either
4521 # via an interactive prompt or by command line flags.
4522 if options.dry_run:
4523 print('\nNo changes were made (dry run).\n')
4524 return 0
4525 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004526 print('You are currently on a branch \'%s\' which is associated with a '
4527 'closed codereview issue, so archive cannot proceed. Please '
4528 'checkout another branch and run this command again.' %
4529 current_branch)
4530 return 1
kmarshall9249e012016-08-23 12:02:16 -07004531 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004532 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4533 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004534 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004535 return 1
4536
4537 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004538 if not options.notags:
4539 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004540 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004541
vapiera7fbd5a2016-06-16 09:17:49 -07004542 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004543
4544 return 0
4545
4546
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004547@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004548def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004549 """Show status of changelists.
4550
4551 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004552 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004553 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004554 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004555 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004556 - Magenta in the commit queue
4557 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004558 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004559
4560 Also see 'git cl comments'.
4561 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004562 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004563 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004564 parser.add_option('-f', '--fast', action='store_true',
4565 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004566 parser.add_option(
4567 '-j', '--maxjobs', action='store', type=int,
4568 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004569
4570 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004571 _add_codereview_issue_select_options(
4572 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004573 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004574 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004575 if args:
4576 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004577 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004578
iannuccie53c9352016-08-17 14:40:40 -07004579 if options.issue is not None and not options.field:
4580 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004582 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004583 cl = Changelist(auth_config=auth_config, issue=options.issue,
4584 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004585 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004586 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004587 elif options.field == 'id':
4588 issueid = cl.GetIssue()
4589 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004590 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004592 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004593 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004594 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004595 elif options.field == 'status':
4596 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597 elif options.field == 'url':
4598 url = cl.GetIssueURL()
4599 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004600 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004601 return 0
4602
4603 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4604 if not branches:
4605 print('No local branch found.')
4606 return 0
4607
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004608 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004609 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004610 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004611 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004612 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004613 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004614 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004615
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004616 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004617 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4618 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4619 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004620 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004621 c, status = output.next()
4622 branch_statuses[c.GetBranch()] = status
4623 status = branch_statuses.pop(branch)
4624 url = cl.GetIssueURL()
4625 if url and (not status or status == 'error'):
4626 # The issue probably doesn't exist anymore.
4627 url += ' (broken)'
4628
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004629 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004630 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004631 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004632 color = ''
4633 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004634 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004635 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004636 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004637 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004638
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004639
4640 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004641 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004642 print('Current branch: %s' % branch)
4643 for cl in changes:
4644 if cl.GetBranch() == branch:
4645 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004646 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004647 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004648 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004649 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004650 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004651 print('Issue description:')
4652 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653 return 0
4654
4655
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004656def colorize_CMDstatus_doc():
4657 """To be called once in main() to add colors to git cl status help."""
4658 colors = [i for i in dir(Fore) if i[0].isupper()]
4659
4660 def colorize_line(line):
4661 for color in colors:
4662 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004663 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004664 indent = len(line) - len(line.lstrip(' ')) + 1
4665 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4666 return line
4667
4668 lines = CMDstatus.__doc__.splitlines()
4669 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4670
4671
phajdan.jre328cf92016-08-22 04:12:17 -07004672def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004673 if path == '-':
4674 json.dump(contents, sys.stdout)
4675 else:
4676 with open(path, 'w') as f:
4677 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004678
4679
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004680@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004681@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004682def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004683 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004684
4685 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004686 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004687 parser.add_option('-r', '--reverse', action='store_true',
4688 help='Lookup the branch(es) for the specified issues. If '
4689 'no issues are specified, all branches with mapped '
4690 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004691 parser.add_option('--json',
4692 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004693 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004694 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004695 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696
dnj@chromium.org406c4402015-03-03 17:22:28 +00004697 if options.reverse:
4698 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004699 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004700 # Reverse issue lookup.
4701 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004702
4703 git_config = {}
4704 for config in RunGit(['config', '--get-regexp',
4705 r'branch\..*issue']).splitlines():
4706 name, _space, val = config.partition(' ')
4707 git_config[name] = val
4708
dnj@chromium.org406c4402015-03-03 17:22:28 +00004709 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004710 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4711 config_key = _git_branch_config_key(ShortBranchName(branch),
4712 cls.IssueConfigKey())
4713 issue = git_config.get(config_key)
4714 if issue:
4715 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004716 if not args:
4717 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004718 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004719 for issue in args:
4720 if not issue:
4721 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004722 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004723 print('Branch for issue number %s: %s' % (
4724 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004725 if options.json:
4726 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004727 return 0
4728
4729 if len(args) > 0:
4730 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4731 if not issue.valid:
4732 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4733 'or no argument to list it.\n'
4734 'Maybe you want to run git cl status?')
4735 cl = Changelist(codereview=issue.codereview)
4736 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004737 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004738 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004739 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4740 if options.json:
4741 write_json(options.json, {
4742 'issue': cl.GetIssue(),
4743 'issue_url': cl.GetIssueURL(),
4744 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004745 return 0
4746
4747
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004748@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004749def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004750 """Shows or posts review comments for any changelist."""
4751 parser.add_option('-a', '--add-comment', dest='comment',
4752 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004753 parser.add_option('-i', '--issue', dest='issue',
4754 help='review issue id (defaults to current issue). '
4755 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004756 parser.add_option('-m', '--machine-readable', dest='readable',
4757 action='store_false', default=True,
4758 help='output comments in a format compatible with '
4759 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004760 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004761 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004762 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004763 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004764 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004765 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004766 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004767
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004768 issue = None
4769 if options.issue:
4770 try:
4771 issue = int(options.issue)
4772 except ValueError:
4773 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004774 if not options.forced_codereview:
4775 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004776
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004777 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004778 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004779 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004780
4781 if options.comment:
4782 cl.AddComment(options.comment)
4783 return 0
4784
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004785 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4786 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004787 for comment in summary:
4788 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004789 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004790 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004791 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004792 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004793 color = Fore.MAGENTA
4794 else:
4795 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004796 print('\n%s%s %s%s\n%s' % (
4797 color,
4798 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4799 comment.sender,
4800 Fore.RESET,
4801 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4802
smut@google.comc85ac942015-09-15 16:34:43 +00004803 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004804 def pre_serialize(c):
4805 dct = c.__dict__.copy()
4806 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4807 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004808 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004809 return 0
4810
4811
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004812@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004813@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004814def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004815 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004816 parser.add_option('-d', '--display', action='store_true',
4817 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004818 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004819 help='New description to set for this issue (- for stdin, '
4820 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004821 parser.add_option('-f', '--force', action='store_true',
4822 help='Delete any unpublished Gerrit edits for this issue '
4823 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004824
4825 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004826 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004827 options, args = parser.parse_args(args)
4828 _process_codereview_select_options(parser, options)
4829
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004830 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004831 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004832 target_issue_arg = ParseIssueNumberArgument(args[0],
4833 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004834 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004835 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004836
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004837 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004838
martiniss6eda05f2016-06-30 10:18:35 -07004839 kwargs = {
4840 'auth_config': auth_config,
4841 'codereview': options.forced_codereview,
4842 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004843 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004844 if target_issue_arg:
4845 kwargs['issue'] = target_issue_arg.issue
4846 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004847 if target_issue_arg.codereview and not options.forced_codereview:
4848 detected_codereview_from_url = True
4849 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004850
4851 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004852 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004853 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004854 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004855
4856 if detected_codereview_from_url:
4857 logging.info('canonical issue/change URL: %s (type: %s)\n',
4858 cl.GetIssueURL(), target_issue_arg.codereview)
4859
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004860 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004861
smut@google.com34fb6b12015-07-13 20:03:26 +00004862 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004863 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004864 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004865
4866 if options.new_description:
4867 text = options.new_description
4868 if text == '-':
4869 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004870 elif text == '+':
4871 base_branch = cl.GetCommonAncestorWithUpstream()
4872 change = cl.GetChange(base_branch, None, local_description=True)
4873 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004874
4875 description.set_description(text)
4876 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004877 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004878
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004879 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004880 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004881 return 0
4882
4883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004884def CreateDescriptionFromLog(args):
4885 """Pulls out the commit log to use as a base for the CL description."""
4886 log_args = []
4887 if len(args) == 1 and not args[0].endswith('.'):
4888 log_args = [args[0] + '..']
4889 elif len(args) == 1 and args[0].endswith('...'):
4890 log_args = [args[0][:-1]]
4891 elif len(args) == 2:
4892 log_args = [args[0] + '..' + args[1]]
4893 else:
4894 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004895 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004896
4897
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004898@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004899def CMDlint(parser, args):
4900 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004901 parser.add_option('--filter', action='append', metavar='-x,+y',
4902 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004903 auth.add_auth_options(parser)
4904 options, args = parser.parse_args(args)
4905 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004906
4907 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004908 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004909 try:
4910 import cpplint
4911 import cpplint_chromium
4912 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004913 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004914 return 1
4915
4916 # Change the current working directory before calling lint so that it
4917 # shows the correct base.
4918 previous_cwd = os.getcwd()
4919 os.chdir(settings.GetRoot())
4920 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004921 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004922 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4923 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004924 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004925 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004926 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004927
4928 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004929 command = args + files
4930 if options.filter:
4931 command = ['--filter=' + ','.join(options.filter)] + command
4932 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004933
4934 white_regex = re.compile(settings.GetLintRegex())
4935 black_regex = re.compile(settings.GetLintIgnoreRegex())
4936 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4937 for filename in filenames:
4938 if white_regex.match(filename):
4939 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004940 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004941 else:
4942 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4943 extra_check_functions)
4944 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004945 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004946 finally:
4947 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004948 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004949 if cpplint._cpplint_state.error_count != 0:
4950 return 1
4951 return 0
4952
4953
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004954@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004955def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004956 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004957 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004958 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004959 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004960 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004961 parser.add_option('--all', action='store_true',
4962 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004963 parser.add_option('--parallel', action='store_true',
4964 help='Run all tests specified by input_api.RunTests in all '
4965 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004966 auth.add_auth_options(parser)
4967 options, args = parser.parse_args(args)
4968 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004969
sbc@chromium.org71437c02015-04-09 19:29:40 +00004970 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004971 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004972 return 1
4973
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004974 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004975 if args:
4976 base_branch = args[0]
4977 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004978 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004979 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004980
Aaron Gable8076c282017-11-29 14:39:41 -08004981 if options.all:
4982 base_change = cl.GetChange(base_branch, None)
4983 files = [('M', f) for f in base_change.AllFiles()]
4984 change = presubmit_support.GitChange(
4985 base_change.Name(),
4986 base_change.FullDescriptionText(),
4987 base_change.RepositoryRoot(),
4988 files,
4989 base_change.issue,
4990 base_change.patchset,
4991 base_change.author_email,
4992 base_change._upstream)
4993 else:
4994 change = cl.GetChange(base_branch, None)
4995
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004996 cl.RunHook(
4997 committing=not options.upload,
4998 may_prompt=False,
4999 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04005000 change=change,
5001 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00005002 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005003
5004
tandrii@chromium.org65874e12016-03-04 12:03:02 +00005005def GenerateGerritChangeId(message):
5006 """Returns Ixxxxxx...xxx change id.
5007
5008 Works the same way as
5009 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
5010 but can be called on demand on all platforms.
5011
5012 The basic idea is to generate git hash of a state of the tree, original commit
5013 message, author/committer info and timestamps.
5014 """
5015 lines = []
5016 tree_hash = RunGitSilent(['write-tree'])
5017 lines.append('tree %s' % tree_hash.strip())
5018 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
5019 if code == 0:
5020 lines.append('parent %s' % parent.strip())
5021 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
5022 lines.append('author %s' % author.strip())
5023 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
5024 lines.append('committer %s' % committer.strip())
5025 lines.append('')
5026 # Note: Gerrit's commit-hook actually cleans message of some lines and
5027 # whitespace. This code is not doing this, but it clearly won't decrease
5028 # entropy.
5029 lines.append(message)
5030 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
5031 stdin='\n'.join(lines))
5032 return 'I%s' % change_hash.strip()
5033
5034
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005035def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00005036 """Computes the remote branch ref to use for the CL.
5037
5038 Args:
5039 remote (str): The git remote for the CL.
5040 remote_branch (str): The git remote branch for the CL.
5041 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00005042 """
5043 if not (remote and remote_branch):
5044 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005045
wittman@chromium.org455dc922015-01-26 20:15:50 +00005046 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005047 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00005048 # refs, which are then translated into the remote full symbolic refs
5049 # below.
5050 if '/' not in target_branch:
5051 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
5052 else:
5053 prefix_replacements = (
5054 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
5055 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
5056 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
5057 )
5058 match = None
5059 for regex, replacement in prefix_replacements:
5060 match = re.search(regex, target_branch)
5061 if match:
5062 remote_branch = target_branch.replace(match.group(0), replacement)
5063 break
5064 if not match:
5065 # This is a branch path but not one we recognize; use as-is.
5066 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00005067 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
5068 # Handle the refs that need to land in different refs.
5069 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005070
wittman@chromium.org455dc922015-01-26 20:15:50 +00005071 # Create the true path to the remote branch.
5072 # Does the following translation:
5073 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
5074 # * refs/remotes/origin/master -> refs/heads/master
5075 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
5076 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
5077 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
5078 elif remote_branch.startswith('refs/remotes/%s/' % remote):
5079 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
5080 'refs/heads/')
5081 elif remote_branch.startswith('refs/remotes/branch-heads'):
5082 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01005083
wittman@chromium.org455dc922015-01-26 20:15:50 +00005084 return remote_branch
5085
5086
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005087def cleanup_list(l):
5088 """Fixes a list so that comma separated items are put as individual items.
5089
5090 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
5091 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
5092 """
5093 items = sum((i.split(',') for i in l), [])
5094 stripped_items = (i.strip() for i in items)
5095 return sorted(filter(None, stripped_items))
5096
5097
Aaron Gable4db38df2017-11-03 14:59:07 -07005098@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005099@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005100def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00005101 """Uploads the current changelist to codereview.
5102
5103 Can skip dependency patchset uploads for a branch by running:
5104 git config branch.branch_name.skip-deps-uploads True
5105 To unset run:
5106 git config --unset branch.branch_name.skip-deps-uploads
5107 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02005108
5109 If the name of the checked out branch starts with "bug-" or "fix-" followed by
5110 a bug number, this bug number is automatically populated in the CL
5111 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005112
5113 If subject contains text in square brackets or has "<text>: " prefix, such
5114 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
5115 [git-cl] add support for hashtags
5116 Foo bar: implement foo
5117 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00005118 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00005119 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5120 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00005121 parser.add_option('--bypass-watchlists', action='store_true',
5122 dest='bypass_watchlists',
5123 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005124 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00005125 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005126 parser.add_option('--message', '-m', dest='message',
5127 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07005128 parser.add_option('-b', '--bug',
5129 help='pre-populate the bug number(s) for this issue. '
5130 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07005131 parser.add_option('--message-file', dest='message_file',
5132 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005133 parser.add_option('--title', '-t', dest='title',
5134 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005135 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005136 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005137 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005138 parser.add_option('--tbrs',
5139 action='append', default=[],
5140 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005141 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005142 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005143 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005144 parser.add_option('--hashtag', dest='hashtags',
5145 action='append', default=[],
5146 help=('Gerrit hashtag for new CL; '
5147 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005148 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005149 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005150 parser.add_option('--emulate_svn_auto_props',
5151 '--emulate-svn-auto-props',
5152 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005153 dest="emulate_svn_auto_props",
5154 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005155 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005156 help='tell the commit queue to commit this patchset; '
5157 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005158 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005159 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005160 metavar='TARGET',
5161 help='Apply CL to remote ref TARGET. ' +
5162 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005163 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005164 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005165 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005166 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005167 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005168 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005169 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5170 const='TBR', help='add a set of OWNERS to TBR')
5171 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5172 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005173 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5174 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005175 help='Send the patchset to do a CQ dry run right after '
5176 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005177 parser.add_option('--dependencies', action='store_true',
5178 help='Uploads CLs of all the local branches that depend on '
5179 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005180 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5181 help='Sends your change to the CQ after an approval. Only '
5182 'works on repos that have the Auto-Submit label '
5183 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005184 parser.add_option('--parallel', action='store_true',
5185 help='Run all tests specified by input_api.RunTests in all '
5186 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005187
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005188 parser.add_option('--no-autocc', action='store_true',
5189 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005190 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005191 help='Set the review private. This implies --no-autocc.')
5192
5193 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005194 parser.add_option('--email', default=None,
5195 help='email address to use to connect to Rietveld')
5196
rmistry@google.com2dd99862015-06-22 12:22:18 +00005197 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005198 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005199 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005200 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005201 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005202 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005203
sbc@chromium.org71437c02015-04-09 19:29:40 +00005204 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005205 return 1
5206
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005207 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005208 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005209 options.cc = cleanup_list(options.cc)
5210
tandriib80458a2016-06-23 12:20:07 -07005211 if options.message_file:
5212 if options.message:
5213 parser.error('only one of --message and --message-file allowed.')
5214 options.message = gclient_utils.FileRead(options.message_file)
5215 options.message_file = None
5216
tandrii4d0545a2016-07-06 03:56:49 -07005217 if options.cq_dry_run and options.use_commit_queue:
5218 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5219
Aaron Gableedbc4132017-09-11 13:22:28 -07005220 if options.use_commit_queue:
5221 options.send_mail = True
5222
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005223 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5224 settings.GetIsGerrit()
5225
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005226 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005227 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005228
5229
Francois Dorayd42c6812017-05-30 15:10:20 -04005230@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005231@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005232def CMDsplit(parser, args):
5233 """Splits a branch into smaller branches and uploads CLs.
5234
5235 Creates a branch and uploads a CL for each group of files modified in the
5236 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005237 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005238 the shared OWNERS file.
5239 """
5240 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005241 help="A text file containing a CL description in which "
5242 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005243 parser.add_option("-c", "--comment", dest="comment_file",
5244 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005245 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5246 default=False,
5247 help="List the files and reviewers for each CL that would "
5248 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005249 parser.add_option("--cq-dry-run", action='store_true',
5250 help="If set, will do a cq dry run for each uploaded CL. "
5251 "Please be careful when doing this; more than ~10 CLs "
5252 "has the potential to overload our build "
5253 "infrastructure. Try to upload these not during high "
5254 "load times (usually 11-3 Mountain View time). Email "
5255 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005256 options, _ = parser.parse_args(args)
5257
5258 if not options.description_file:
5259 parser.error('No --description flag specified.')
5260
5261 def WrappedCMDupload(args):
5262 return CMDupload(OptionParser(), args)
5263
5264 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005265 Changelist, WrappedCMDupload, options.dry_run,
5266 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005267
5268
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005269@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005270@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005271def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005272 """DEPRECATED: Used to commit the current changelist via git-svn."""
5273 message = ('git-cl no longer supports committing to SVN repositories via '
5274 'git-svn. You probably want to use `git cl land` instead.')
5275 print(message)
5276 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005277
5278
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005279# Two special branches used by git cl land.
5280MERGE_BRANCH = 'git-cl-commit'
5281CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5282
5283
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005284@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005285@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005286def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005287 """Commits the current changelist via git.
5288
5289 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5290 upstream and closes the issue automatically and atomically.
5291
5292 Otherwise (in case of Rietveld):
5293 Squashes branch into a single commit.
5294 Updates commit message with metadata (e.g. pointer to review).
5295 Pushes the code upstream.
5296 Updates review and closes.
5297 """
5298 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5299 help='bypass upload presubmit hook')
5300 parser.add_option('-m', dest='message',
5301 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005302 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005303 help="force yes to questions (don't prompt)")
5304 parser.add_option('-c', dest='contributor',
5305 help="external contributor for patch (appended to " +
5306 "description and used as author for git). Should be " +
5307 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005308 parser.add_option('--parallel', action='store_true',
5309 help='Run all tests specified by input_api.RunTests in all '
5310 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005311 auth.add_auth_options(parser)
5312 (options, args) = parser.parse_args(args)
5313 auth_config = auth.extract_auth_config_from_options(options)
5314
5315 cl = Changelist(auth_config=auth_config)
5316
Robert Iannucci2e73d432018-03-14 01:10:47 -07005317 if not cl.IsGerrit():
5318 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005319
Robert Iannucci2e73d432018-03-14 01:10:47 -07005320 if options.message:
5321 # This could be implemented, but it requires sending a new patch to
5322 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5323 # Besides, Gerrit has the ability to change the commit message on submit
5324 # automatically, thus there is no need to support this option (so far?).
5325 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005326 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005327 parser.error(
5328 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5329 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5330 'the contributor\'s "name <email>". If you can\'t upload such a '
5331 'commit for review, contact your repository admin and request'
5332 '"Forge-Author" permission.')
5333 if not cl.GetIssue():
5334 DieWithError('You must upload the change first to Gerrit.\n'
5335 ' If you would rather have `git cl land` upload '
5336 'automatically for you, see http://crbug.com/642759')
5337 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005338 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005339
5340
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005341def PushToGitWithAutoRebase(remote, branch, original_description,
5342 git_numberer_enabled, max_attempts=3):
5343 """Pushes current HEAD commit on top of remote's branch.
5344
5345 Attempts to fetch and autorebase on push failures.
5346 Adds git number footers on the fly.
5347
5348 Returns integer code from last command.
5349 """
5350 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5351 code = 0
5352 attempts_left = max_attempts
5353 while attempts_left:
5354 attempts_left -= 1
5355 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5356
5357 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5358 # If fetch fails, retry.
5359 print('Fetching %s/%s...' % (remote, branch))
5360 code, out = RunGitWithCode(
5361 ['retry', 'fetch', remote,
5362 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5363 if code:
5364 print('Fetch failed with exit code %d.' % code)
5365 print(out.strip())
5366 continue
5367
5368 print('Cherry-picking commit on top of latest %s' % branch)
5369 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5370 suppress_stderr=True)
5371 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5372 code, out = RunGitWithCode(['cherry-pick', cherry])
5373 if code:
5374 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5375 'the following files have merge conflicts:' %
5376 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005377 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5378 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005379 print('Please rebase your patch and try again.')
5380 RunGitWithCode(['cherry-pick', '--abort'])
5381 break
5382
5383 commit_desc = ChangeDescription(original_description)
5384 if git_numberer_enabled:
5385 logging.debug('Adding git number footers')
5386 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5387 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5388 branch)
5389 # Ensure timestamps are monotonically increasing.
5390 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5391 _get_committer_timestamp('HEAD'))
5392 _git_amend_head(commit_desc.description, timestamp)
5393
5394 code, out = RunGitWithCode(
5395 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5396 print(out)
5397 if code == 0:
5398 break
5399 if IsFatalPushFailure(out):
5400 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005401 'user.email are correct and you have push access to the repo.\n'
5402 'Hint: run command below to diangose common Git/Gerrit credential '
5403 'problems:\n'
5404 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005405 break
5406 return code
5407
5408
5409def IsFatalPushFailure(push_stdout):
5410 """True if retrying push won't help."""
5411 return '(prohibited by Gerrit)' in push_stdout
5412
5413
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005414@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005415@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005416def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005417 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005418 parser.add_option('-b', dest='newbranch',
5419 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005420 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005421 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005422 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005423 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005424 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005425 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005426 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005427 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005428 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005429 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005430
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005431
5432 group = optparse.OptionGroup(
5433 parser,
5434 'Options for continuing work on the current issue uploaded from a '
5435 'different clone (e.g. different machine). Must be used independently '
5436 'from the other options. No issue number should be specified, and the '
5437 'branch must have an issue number associated with it')
5438 group.add_option('--reapply', action='store_true', dest='reapply',
5439 help='Reset the branch and reapply the issue.\n'
5440 'CAUTION: This will undo any local changes in this '
5441 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005442
5443 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005444 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005445 parser.add_option_group(group)
5446
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005447 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005448 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005449 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005450 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005451 auth_config = auth.extract_auth_config_from_options(options)
5452
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005453 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005454 if options.newbranch:
5455 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005456 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005457 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005458
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005459 cl = Changelist(auth_config=auth_config,
5460 codereview=options.forced_codereview)
5461 if not cl.GetIssue():
5462 parser.error('current branch must have an associated issue')
5463
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005464 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005465 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005466 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005467
5468 RunGit(['reset', '--hard', upstream])
5469 if options.pull:
5470 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005471
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005472 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5473 options.directory)
5474
5475 if len(args) != 1 or not args[0]:
5476 parser.error('Must specify issue number or url')
5477
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005478 target_issue_arg = ParseIssueNumberArgument(args[0],
5479 options.forced_codereview)
5480 if not target_issue_arg.valid:
5481 parser.error('invalid codereview url or CL id')
5482
5483 cl_kwargs = {
5484 'auth_config': auth_config,
5485 'codereview_host': target_issue_arg.hostname,
5486 'codereview': options.forced_codereview,
5487 }
5488 detected_codereview_from_url = False
5489 if target_issue_arg.codereview and not options.forced_codereview:
5490 detected_codereview_from_url = True
5491 cl_kwargs['codereview'] = target_issue_arg.codereview
5492 cl_kwargs['issue'] = target_issue_arg.issue
5493
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005494 # We don't want uncommitted changes mixed up with the patch.
5495 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005496 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005497
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005498 if options.newbranch:
5499 if options.force:
5500 RunGit(['branch', '-D', options.newbranch],
5501 stderr=subprocess2.PIPE, error_ok=True)
5502 RunGit(['new-branch', options.newbranch])
5503
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005504 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005505
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005506 if cl.IsGerrit():
5507 if options.reject:
5508 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005509 if options.directory:
5510 parser.error('--directory is not supported with Gerrit codereview.')
5511
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005512 if detected_codereview_from_url:
5513 print('canonical issue/change URL: %s (type: %s)\n' %
5514 (cl.GetIssueURL(), target_issue_arg.codereview))
5515
5516 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005517 options.nocommit, options.directory,
5518 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005519
5520
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005521def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005522 """Fetches the tree status and returns either 'open', 'closed',
5523 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005524 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005525 if url:
5526 status = urllib2.urlopen(url).read().lower()
5527 if status.find('closed') != -1 or status == '0':
5528 return 'closed'
5529 elif status.find('open') != -1 or status == '1':
5530 return 'open'
5531 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005532 return 'unset'
5533
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005534
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005535def GetTreeStatusReason():
5536 """Fetches the tree status from a json url and returns the message
5537 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005538 url = settings.GetTreeStatusUrl()
5539 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005540 connection = urllib2.urlopen(json_url)
5541 status = json.loads(connection.read())
5542 connection.close()
5543 return status['message']
5544
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005545
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005546@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005547def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005548 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005549 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005550 status = GetTreeStatus()
5551 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005552 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005553 return 2
5554
vapiera7fbd5a2016-06-16 09:17:49 -07005555 print('The tree is %s' % status)
5556 print()
5557 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005558 if status != 'open':
5559 return 1
5560 return 0
5561
5562
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005563@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005564def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005565 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005566 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005567 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005568 '-b', '--bot', action='append',
5569 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5570 'times to specify multiple builders. ex: '
5571 '"-b win_rel -b win_layout". See '
5572 'the try server waterfall for the builders name and the tests '
5573 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005574 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005575 '-B', '--bucket', default='',
5576 help=('Buildbucket bucket to send the try requests.'))
5577 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005578 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005579 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005580 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005581 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005582 help='Revision to use for the try job; default: the revision will '
5583 'be determined by the try recipe that builder runs, which usually '
5584 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005585 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005586 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005587 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005588 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005589 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005590 '--category', default='git_cl_try', help='Specify custom build category.')
5591 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005592 '--project',
5593 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005594 'in recipe to determine to which repository or directory to '
5595 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005596 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005597 '-p', '--property', dest='properties', action='append', default=[],
5598 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005599 'key2=value2 etc. The value will be treated as '
5600 'json if decodable, or as string otherwise. '
5601 'NOTE: using this may make your try job not usable for CQ, '
5602 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005603 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005604 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5605 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005606 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005607 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005608 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005609 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005610 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005611 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005612
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005613 if options.master and options.master.startswith('luci.'):
5614 parser.error(
5615 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005616 # Make sure that all properties are prop=value pairs.
5617 bad_params = [x for x in options.properties if '=' not in x]
5618 if bad_params:
5619 parser.error('Got properties with missing "=": %s' % bad_params)
5620
maruel@chromium.org15192402012-09-06 12:38:29 +00005621 if args:
5622 parser.error('Unknown arguments: %s' % args)
5623
Koji Ishii31c14782018-01-08 17:17:33 +09005624 cl = Changelist(auth_config=auth_config, issue=options.issue,
5625 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005626 if not cl.GetIssue():
5627 parser.error('Need to upload first')
5628
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005629 if cl.IsGerrit():
5630 # HACK: warm up Gerrit change detail cache to save on RPCs.
5631 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5632
tandriie113dfd2016-10-11 10:20:12 -07005633 error_message = cl.CannotTriggerTryJobReason()
5634 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005635 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005636
borenet6c0efe62016-10-19 08:13:29 -07005637 if options.bucket and options.master:
5638 parser.error('Only one of --bucket and --master may be used.')
5639
qyearsley1fdfcb62016-10-24 13:22:03 -07005640 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005641
qyearsleydd49f942016-10-28 11:57:22 -07005642 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5643 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005644 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005645 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005646 print('git cl try with no bots now defaults to CQ dry run.')
5647 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5648 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005649
borenet6c0efe62016-10-19 08:13:29 -07005650 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005651 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005652 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005653 'of bot requires an initial job from a parent (usually a builder). '
5654 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005655 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005656 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005657
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005658 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005659 # TODO(tandrii): Checking local patchset against remote patchset is only
5660 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5661 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005662 print('Warning: Codereview server has newer patchsets (%s) than most '
5663 'recent upload from local checkout (%s). Did a previous upload '
5664 'fail?\n'
5665 'By default, git cl try uses the latest patchset from '
5666 'codereview, continuing to use patchset %s.\n' %
5667 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005668
tandrii568043b2016-10-11 07:49:18 -07005669 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005670 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005671 except BuildbucketResponseException as ex:
5672 print('ERROR: %s' % ex)
5673 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005674 return 0
5675
5676
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005677@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005678def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005679 """Prints info about try jobs associated with current CL."""
5680 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005681 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005682 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005683 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005684 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005686 '--color', action='store_true', default=setup_color.IS_TTY,
5687 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005688 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005689 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5690 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005691 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005692 '--json', help=('Path of JSON output file to write try job results to,'
5693 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005694 parser.add_option_group(group)
5695 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005696 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005697 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005698 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005699 if args:
5700 parser.error('Unrecognized args: %s' % ' '.join(args))
5701
5702 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005703 cl = Changelist(
5704 issue=options.issue, codereview=options.forced_codereview,
5705 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005706 if not cl.GetIssue():
5707 parser.error('Need to upload first')
5708
tandrii221ab252016-10-06 08:12:04 -07005709 patchset = options.patchset
5710 if not patchset:
5711 patchset = cl.GetMostRecentPatchset()
5712 if not patchset:
5713 parser.error('Codereview doesn\'t know about issue %s. '
5714 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005715 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005716 cl.GetIssue())
5717
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005718 # TODO(tandrii): Checking local patchset against remote patchset is only
5719 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5720 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005721 print('Warning: Codereview server has newer patchsets (%s) than most '
5722 'recent upload from local checkout (%s). Did a previous upload '
5723 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005724 'By default, git cl try-results uses the latest patchset from '
5725 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005726 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005727 try:
tandrii221ab252016-10-06 08:12:04 -07005728 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005729 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005730 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005731 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005732 if options.json:
5733 write_try_results_json(options.json, jobs)
5734 else:
5735 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005736 return 0
5737
5738
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005739@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005740@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005741def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005742 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005743 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005744 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005745 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005747 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005748 if args:
5749 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005750 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005751 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005752 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005753 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005754
5755 # Clear configured merge-base, if there is one.
5756 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005757 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005758 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005759 return 0
5760
5761
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005762@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005763def CMDweb(parser, args):
5764 """Opens the current CL in the web browser."""
5765 _, args = parser.parse_args(args)
5766 if args:
5767 parser.error('Unrecognized args: %s' % ' '.join(args))
5768
5769 issue_url = Changelist().GetIssueURL()
5770 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005771 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005772 return 1
5773
5774 webbrowser.open(issue_url)
5775 return 0
5776
5777
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005778@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005779def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005780 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005781 parser.add_option('-d', '--dry-run', action='store_true',
5782 help='trigger in dry run mode')
5783 parser.add_option('-c', '--clear', action='store_true',
5784 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005785 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005786 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005787 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005788 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005789 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005790 if args:
5791 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005792 if options.dry_run and options.clear:
5793 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5794
iannuccie53c9352016-08-17 14:40:40 -07005795 cl = Changelist(auth_config=auth_config, issue=options.issue,
5796 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005797 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005798 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005799 elif options.dry_run:
5800 state = _CQState.DRY_RUN
5801 else:
5802 state = _CQState.COMMIT
5803 if not cl.GetIssue():
5804 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005805 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005806 return 0
5807
5808
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005809@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005810def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005811 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005812 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005813 auth.add_auth_options(parser)
5814 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005815 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005816 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005817 if args:
5818 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005819 cl = Changelist(auth_config=auth_config, issue=options.issue,
5820 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005821 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005822 if not cl.GetIssue():
5823 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005824 cl.CloseIssue()
5825 return 0
5826
5827
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005828@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005829def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005830 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005831 parser.add_option(
5832 '--stat',
5833 action='store_true',
5834 dest='stat',
5835 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005836 auth.add_auth_options(parser)
5837 options, args = parser.parse_args(args)
5838 auth_config = auth.extract_auth_config_from_options(options)
5839 if args:
5840 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005841
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005842 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005843 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005844 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005845 if not issue:
5846 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005847
Aaron Gablea718c3e2017-08-28 17:47:28 -07005848 base = cl._GitGetBranchConfigValue('last-upload-hash')
5849 if not base:
5850 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5851 if not base:
5852 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5853 revision_info = detail['revisions'][detail['current_revision']]
5854 fetch_info = revision_info['fetch']['http']
5855 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5856 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005857
Aaron Gablea718c3e2017-08-28 17:47:28 -07005858 cmd = ['git', 'diff']
5859 if options.stat:
5860 cmd.append('--stat')
5861 cmd.append(base)
5862 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005863
5864 return 0
5865
5866
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005867@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005868def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005869 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005870 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005871 '--ignore-current',
5872 action='store_true',
5873 help='Ignore the CL\'s current reviewers and start from scratch.')
5874 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005875 '--no-color',
5876 action='store_true',
5877 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005878 parser.add_option(
5879 '--batch',
5880 action='store_true',
5881 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005882 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005883 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005884 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005885
5886 author = RunGit(['config', 'user.email']).strip() or None
5887
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005888 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005889
5890 if args:
5891 if len(args) > 1:
5892 parser.error('Unknown args')
5893 base_branch = args[0]
5894 else:
5895 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005896 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005897
5898 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005899 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5900
5901 if options.batch:
5902 db = owners.Database(change.RepositoryRoot(), file, os.path)
5903 print('\n'.join(db.reviewers_for(affected_files, author)))
5904 return 0
5905
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005906 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005907 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005908 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005909 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005910 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005911 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005912 disable_color=options.no_color,
5913 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005914
5915
Aiden Bennerc08566e2018-10-03 17:52:42 +00005916def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005917 """Generates a diff command."""
5918 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005919 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5920
5921 if not allow_prefix:
5922 diff_cmd += ['--no-prefix']
5923
5924 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005925
5926 if args:
5927 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005928 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005929 diff_cmd.append(arg)
5930 else:
5931 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005932
5933 return diff_cmd
5934
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005935
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005936def MatchingFileType(file_name, extensions):
5937 """Returns true if the file name ends with one of the given extensions."""
5938 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005939
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005940
enne@chromium.org555cfe42014-01-29 18:21:39 +00005941@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005942@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005943def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005944 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005945 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005946 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005947 parser.add_option('--full', action='store_true',
5948 help='Reformat the full content of all touched files')
5949 parser.add_option('--dry-run', action='store_true',
5950 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005951 parser.add_option('--python', action='store_true',
5952 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005953 parser.add_option('--js', action='store_true',
5954 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005955 parser.add_option('--diff', action='store_true',
5956 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005957 parser.add_option('--presubmit', action='store_true',
5958 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005959 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005960
Daniel Chengc55eecf2016-12-30 03:11:02 -08005961 # Normalize any remaining args against the current path, so paths relative to
5962 # the current directory are still resolved as expected.
5963 args = [os.path.join(os.getcwd(), arg) for arg in args]
5964
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005965 # git diff generates paths against the root of the repository. Change
5966 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005967 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005968 if rel_base_path:
5969 os.chdir(rel_base_path)
5970
digit@chromium.org29e47272013-05-17 17:01:46 +00005971 # Grab the merge-base commit, i.e. the upstream commit of the current
5972 # branch when it was created or the last time it was rebased. This is
5973 # to cover the case where the user may have called "git fetch origin",
5974 # moving the origin branch to a newer commit, but hasn't rebased yet.
5975 upstream_commit = None
5976 cl = Changelist()
5977 upstream_branch = cl.GetUpstreamBranch()
5978 if upstream_branch:
5979 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5980 upstream_commit = upstream_commit.strip()
5981
5982 if not upstream_commit:
5983 DieWithError('Could not find base commit for this branch. '
5984 'Are you in detached state?')
5985
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005986 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5987 diff_output = RunGit(changed_files_cmd)
5988 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005989 # Filter out files deleted by this CL
5990 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005991
Christopher Lamc5ba6922017-01-24 11:19:14 +11005992 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005993 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005994
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005995 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5996 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5997 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005998 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005999
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00006000 top_dir = os.path.normpath(
6001 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
6002
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006003 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6004 # formatted. This is used to block during the presubmit.
6005 return_value = 0
6006
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006007 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006008 # Locate the clang-format binary in the checkout
6009 try:
6010 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07006011 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006012 DieWithError(e)
6013
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006014 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006015 cmd = [clang_format_tool]
6016 if not opts.dry_run and not opts.diff:
6017 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006018 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006019 if opts.diff:
6020 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006021 else:
6022 env = os.environ.copy()
6023 env['PATH'] = str(os.path.dirname(clang_format_tool))
6024 try:
6025 script = clang_format.FindClangFormatScriptInChromiumTree(
6026 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006027 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006028 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006029
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006030 cmd = [sys.executable, script, '-p0']
6031 if not opts.dry_run and not opts.diff:
6032 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006033
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006034 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6035 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006036
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006037 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6038 if opts.diff:
6039 sys.stdout.write(stdout)
6040 if opts.dry_run and len(stdout) > 0:
6041 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006042
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006043 # Similar code to above, but using yapf on .py files rather than clang-format
6044 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00006045 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006046 yapf_tool = gclient_utils.FindExecutable('yapf')
6047 if yapf_tool is None:
6048 DieWithError('yapf not found in PATH')
6049
Aiden Bennerc08566e2018-10-03 17:52:42 +00006050 # If we couldn't find a yapf file we'll default to the chromium style
6051 # specified in depot_tools.
6052 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6053 chromium_default_yapf_style = os.path.join(depot_tools_path,
6054 YAPF_CONFIG_FILENAME)
6055
6056 # Note: yapf still seems to fix indentation of the entire file
6057 # even if line ranges are specified.
6058 # See https://github.com/google/yapf/issues/499
6059 if not opts.full:
6060 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
6061
6062 # Used for caching.
6063 yapf_configs = {}
6064 for f in python_diff_files:
6065 # Find the yapf style config for the current file, defaults to depot
6066 # tools default.
6067 yapf_config = _FindYapfConfigFile(
6068 os.path.abspath(f), yapf_configs, top_dir,
6069 chromium_default_yapf_style)
6070
6071 cmd = [yapf_tool, '--style', yapf_config, f]
6072
6073 has_formattable_lines = False
6074 if not opts.full:
6075 # Only run yapf over changed line ranges.
6076 for diff_start, diff_len in py_line_diffs[f]:
6077 diff_end = diff_start + diff_len - 1
6078 # Yapf errors out if diff_end < diff_start but this
6079 # is a valid line range diff for a removal.
6080 if diff_end >= diff_start:
6081 has_formattable_lines = True
6082 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6083 # If all line diffs were removals we have nothing to format.
6084 if not has_formattable_lines:
6085 continue
6086
6087 if opts.diff or opts.dry_run:
6088 cmd += ['--diff']
6089 # Will return non-zero exit code if non-empty diff.
6090 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
6091 if opts.diff:
6092 sys.stdout.write(stdout)
6093 elif len(stdout) > 0:
6094 return_value = 2
6095 else:
6096 cmd += ['-i']
6097 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006098
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006099 # Dart's formatter does not have the nice property of only operating on
6100 # modified chunks, so hard code full.
6101 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006102 try:
6103 command = [dart_format.FindDartFmtToolInChromiumTree()]
6104 if not opts.dry_run and not opts.diff:
6105 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006106 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006107
ppi@chromium.org6593d932016-03-03 15:41:15 +00006108 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006109 if opts.dry_run and stdout:
6110 return_value = 2
6111 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006112 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6113 'found in this checkout. Files in other languages are still '
6114 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006115
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006116 # Format GN build files. Always run on full build files for canonical form.
6117 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006118 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006119 if opts.dry_run or opts.diff:
6120 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006121 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006122 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6123 shell=sys.platform == 'win32',
6124 cwd=top_dir)
6125 if opts.dry_run and gn_ret == 2:
6126 return_value = 2 # Not formatted.
6127 elif opts.diff and gn_ret == 2:
6128 # TODO this should compute and print the actual diff.
6129 print("This change has GN build file diff for " + gn_diff_file)
6130 elif gn_ret != 0:
6131 # For non-dry run cases (and non-2 return values for dry-run), a
6132 # nonzero error code indicates a failure, probably because the file
6133 # doesn't parse.
6134 DieWithError("gn format failed on " + gn_diff_file +
6135 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006136
Ilya Shermane081cbe2017-08-15 17:51:04 -07006137 # Skip the metrics formatting from the global presubmit hook. These files have
6138 # a separate presubmit hook that issues an error if the files need formatting,
6139 # whereas the top-level presubmit script merely issues a warning. Formatting
6140 # these files is somewhat slow, so it's important not to duplicate the work.
6141 if not opts.presubmit:
6142 for xml_dir in GetDirtyMetricsDirs(diff_files):
6143 tool_dir = os.path.join(top_dir, xml_dir)
6144 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6145 if opts.dry_run or opts.diff:
6146 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006147 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006148 if opts.diff:
6149 sys.stdout.write(stdout)
6150 if opts.dry_run and stdout:
6151 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006152
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006153 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006154
Steven Holte2e664bf2017-04-21 13:10:47 -07006155def GetDirtyMetricsDirs(diff_files):
6156 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6157 metrics_xml_dirs = [
6158 os.path.join('tools', 'metrics', 'actions'),
6159 os.path.join('tools', 'metrics', 'histograms'),
6160 os.path.join('tools', 'metrics', 'rappor'),
6161 os.path.join('tools', 'metrics', 'ukm')]
6162 for xml_dir in metrics_xml_dirs:
6163 if any(file.startswith(xml_dir) for file in xml_diff_files):
6164 yield xml_dir
6165
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006166
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006167@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006168@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006169def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006170 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006171 _, args = parser.parse_args(args)
6172
6173 if len(args) != 1:
6174 parser.print_help()
6175 return 1
6176
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006177 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006178 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006179 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006180
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006181 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006182
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006183 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006184 output = RunGit(['config', '--local', '--get-regexp',
6185 r'branch\..*\.%s' % issueprefix],
6186 error_ok=True)
6187 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006188 if issue == target_issue:
6189 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006190
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006191 branches = []
6192 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006193 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006194 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006195 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006196 return 1
6197 if len(branches) == 1:
6198 RunGit(['checkout', branches[0]])
6199 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006200 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006201 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006202 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006203 which = raw_input('Choose by index: ')
6204 try:
6205 RunGit(['checkout', branches[int(which)]])
6206 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006207 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006208 return 1
6209
6210 return 0
6211
6212
maruel@chromium.org29404b52014-09-08 22:58:00 +00006213def CMDlol(parser, args):
6214 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006215 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006216 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6217 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6218 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006219 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006220 return 0
6221
6222
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006223class OptionParser(optparse.OptionParser):
6224 """Creates the option parse and add --verbose support."""
6225 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006226 optparse.OptionParser.__init__(
6227 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006228 self.add_option(
6229 '-v', '--verbose', action='count', default=0,
6230 help='Use 2 times for more debugging info')
6231
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006232 def parse_args(self, args=None, _values=None):
6233 # Create an optparse.Values object that will store only the actual passed
6234 # options, without the defaults.
6235 actual_options = optparse.Values()
6236 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6237 # Create an optparse.Values object with the default options.
6238 options = optparse.Values(self.get_default_values().__dict__)
6239 # Update it with the options passed by the user.
6240 options._update_careful(actual_options.__dict__)
6241 # Store the options passed by the user in an _actual_options attribute.
6242 # We store only the keys, and not the values, since the values can contain
6243 # arbitrary information, which might be PII.
6244 metrics.collector.add('arguments', actual_options.__dict__.keys())
6245
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006246 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006247 logging.basicConfig(
6248 level=levels[min(options.verbose, len(levels) - 1)],
6249 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6250 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006251
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006252 return options, args
6253
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006255def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006256 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006257 print('\nYour python version %s is unsupported, please upgrade.\n' %
6258 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006259 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006260
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006261 # Reload settings.
6262 global settings
6263 settings = Settings()
6264
Edward Lemurad463c92018-07-25 21:31:23 +00006265 if not metrics.DISABLE_METRICS_COLLECTION:
6266 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006267 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006268 dispatcher = subcommand.CommandDispatcher(__name__)
6269 try:
6270 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006271 except auth.AuthenticationError as e:
6272 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006273 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006274 if e.code != 500:
6275 raise
6276 DieWithError(
6277 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6278 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006279 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006280
6281
6282if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006283 # These affect sys.stdout so do it outside of main() to simplify mocks in
6284 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006285 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006286 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006287 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006288 sys.exit(main(sys.argv[1:]))