blob: e85b2411925fe3cd62e7b011fd95399f564b0309 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
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))
tandriide281ae2016-10-12 06:02:30 -0700462 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
463 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
464 hostname=codereview_host,
465 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700467
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700468 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800469 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700470 if options.clobber:
471 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700472 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700473 if extra_properties:
474 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000475
476 batch_req_body = {'builds': []}
477 print_text = []
478 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700479 for bucket, builders_and_tests in sorted(buckets.iteritems()):
480 print_text.append('Bucket: %s' % bucket)
481 master = None
482 if bucket.startswith(MASTER_PREFIX):
483 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000484 for builder, tests in sorted(builders_and_tests.iteritems()):
485 print_text.append(' %s: %s' % (builder, tests))
486 parameters = {
487 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000488 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100489 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'revision': options.revision,
491 }],
tandrii8c5a3532016-11-04 07:52:02 -0700492 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000493 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000494 if 'presubmit' in builder.lower():
495 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000496 if tests:
497 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700498
499 tags = [
500 'builder:%s' % builder,
501 'buildset:%s' % buildset,
502 'user_agent:git_cl_try',
503 ]
504 if master:
505 parameters['properties']['master'] = master
506 tags.append('master:%s' % master)
507
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000508 batch_req_body['builds'].append(
509 {
510 'bucket': bucket,
511 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000512 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700513 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000514 }
515 )
516
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700518 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 http,
520 buildbucket_put_url,
521 'PUT',
522 body=json.dumps(batch_req_body),
523 headers={'Content-Type': 'application/json'}
524 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000525 print_text.append('To see results here, run: git cl try-results')
526 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700527 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000528
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000529
tandrii221ab252016-10-06 08:12:04 -0700530def fetch_try_jobs(auth_config, changelist, buildbucket_host,
531 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700532 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533
qyearsley53f48a12016-09-01 10:45:13 -0700534 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 """
tandrii221ab252016-10-06 08:12:04 -0700536 assert buildbucket_host
537 assert changelist.GetIssue(), 'CL must be uploaded first'
538 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
539 patchset = patchset or changelist.GetMostRecentPatchset()
540 assert patchset, 'CL must be uploaded first'
541
542 codereview_url = changelist.GetCodereviewServer()
543 codereview_host = urlparse.urlparse(codereview_url).hostname
544 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000545 if authenticator.has_cached_credentials():
546 http = authenticator.authorize(httplib2.Http())
547 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700548 print('Warning: Some results might be missing because %s' %
549 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700550 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 http = httplib2.Http()
552
553 http.force_exception_to_status_code = True
554
tandrii221ab252016-10-06 08:12:04 -0700555 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
556 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
557 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700559 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 params = {'tag': 'buildset:%s' % buildset}
561
562 builds = {}
563 while True:
564 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700565 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700567 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 for build in content.get('builds', []):
569 builds[build['id']] = build
570 if 'next_cursor' in content:
571 params['start_cursor'] = content['next_cursor']
572 else:
573 break
574 return builds
575
576
qyearsleyeab3c042016-08-24 09:18:28 -0700577def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 """Prints nicely result of fetch_try_jobs."""
579 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700580 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581 return
582
583 # Make a copy, because we'll be modifying builds dictionary.
584 builds = builds.copy()
585 builder_names_cache = {}
586
587 def get_builder(b):
588 try:
589 return builder_names_cache[b['id']]
590 except KeyError:
591 try:
592 parameters = json.loads(b['parameters_json'])
593 name = parameters['builder_name']
594 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700595 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700596 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 name = None
598 builder_names_cache[b['id']] = name
599 return name
600
601 def get_bucket(b):
602 bucket = b['bucket']
603 if bucket.startswith('master.'):
604 return bucket[len('master.'):]
605 return bucket
606
607 if options.print_master:
608 name_fmt = '%%-%ds %%-%ds' % (
609 max(len(str(get_bucket(b))) for b in builds.itervalues()),
610 max(len(str(get_builder(b))) for b in builds.itervalues()))
611 def get_name(b):
612 return name_fmt % (get_bucket(b), get_builder(b))
613 else:
614 name_fmt = '%%-%ds' % (
615 max(len(str(get_builder(b))) for b in builds.itervalues()))
616 def get_name(b):
617 return name_fmt % get_builder(b)
618
619 def sort_key(b):
620 return b['status'], b.get('result'), get_name(b), b.get('url')
621
622 def pop(title, f, color=None, **kwargs):
623 """Pop matching builds from `builds` dict and print them."""
624
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000625 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000626 colorize = str
627 else:
628 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
629
630 result = []
631 for b in builds.values():
632 if all(b.get(k) == v for k, v in kwargs.iteritems()):
633 builds.pop(b['id'])
634 result.append(b)
635 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700638 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000639
640 total = len(builds)
641 pop(status='COMPLETED', result='SUCCESS',
642 title='Successes:', color=Fore.GREEN,
643 f=lambda b: (get_name(b), b.get('url')))
644 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
645 title='Infra Failures:', color=Fore.MAGENTA,
646 f=lambda b: (get_name(b), b.get('url')))
647 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
648 title='Failures:', color=Fore.RED,
649 f=lambda b: (get_name(b), b.get('url')))
650 pop(status='COMPLETED', result='CANCELED',
651 title='Canceled:', color=Fore.MAGENTA,
652 f=lambda b: (get_name(b),))
653 pop(status='COMPLETED', result='FAILURE',
654 failure_reason='INVALID_BUILD_DEFINITION',
655 title='Wrong master/builder name:', color=Fore.MAGENTA,
656 f=lambda b: (get_name(b),))
657 pop(status='COMPLETED', result='FAILURE',
658 title='Other failures:',
659 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
660 pop(status='COMPLETED',
661 title='Other finished:',
662 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
663 pop(status='STARTED',
664 title='Started:', color=Fore.YELLOW,
665 f=lambda b: (get_name(b), b.get('url')))
666 pop(status='SCHEDULED',
667 title='Scheduled:',
668 f=lambda b: (get_name(b), 'id=%s' % b['id']))
669 # The last section is just in case buildbucket API changes OR there is a bug.
670 pop(title='Other:',
671 f=lambda b: (get_name(b), 'id=%s' % b['id']))
672 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700673 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000674
675
Aiden Bennerc08566e2018-10-03 17:52:42 +0000676def _ComputeDiffLineRanges(files, upstream_commit):
677 """Gets the changed line ranges for each file since upstream_commit.
678
679 Parses a git diff on provided files and returns a dict that maps a file name
680 to an ordered list of range tuples in the form (start_line, count).
681 Ranges are in the same format as a git diff.
682 """
683 # If files is empty then diff_output will be a full diff.
684 if len(files) == 0:
685 return {}
686
687 # Take diff and find the line ranges where there are changes.
688 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
689 diff_output = RunGit(diff_cmd)
690
691 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
692 # 2 capture groups
693 # 0 == fname of diff file
694 # 1 == 'diff_start,diff_count' or 'diff_start'
695 # will match each of
696 # diff --git a/foo.foo b/foo.py
697 # @@ -12,2 +14,3 @@
698 # @@ -12,2 +17 @@
699 # running re.findall on the above string with pattern will give
700 # [('foo.py', ''), ('', '14,3'), ('', '17')]
701
702 curr_file = None
703 line_diffs = {}
704 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
705 if match[0] != '':
706 # Will match the second filename in diff --git a/a.py b/b.py.
707 curr_file = match[0]
708 line_diffs[curr_file] = []
709 else:
710 # Matches +14,3
711 if ',' in match[1]:
712 diff_start, diff_count = match[1].split(',')
713 else:
714 # Single line changes are of the form +12 instead of +12,1.
715 diff_start = match[1]
716 diff_count = 1
717
718 diff_start = int(diff_start)
719 diff_count = int(diff_count)
720
721 # If diff_count == 0 this is a removal we can ignore.
722 line_diffs[curr_file].append((diff_start, diff_count))
723
724 return line_diffs
725
726
727def _FindYapfConfigFile(fpath,
728 yapf_config_cache,
729 top_dir=None,
730 default_style=None):
731 """Checks if a yapf file is in any parent directory of fpath until top_dir.
732
733 Recursively checks parent directories to find yapf file
734 and if no yapf file is found returns default_style.
735 Uses yapf_config_cache as a cache for previously found files.
736 """
737 # Return result if we've already computed it.
738 if fpath in yapf_config_cache:
739 return yapf_config_cache[fpath]
740
741 # Check if there is a style file in the current directory.
742 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
743 dirname = os.path.dirname(fpath)
744 if os.path.isfile(yapf_file):
745 ret = yapf_file
746 elif fpath == top_dir or dirname == fpath:
747 # If we're at the top level directory, or if we're at root
748 # use the chromium default yapf style.
749 ret = default_style
750 else:
751 # Otherwise recurse on the current directory.
752 ret = _FindYapfConfigFile(dirname, yapf_config_cache, top_dir,
753 default_style)
754 yapf_config_cache[fpath] = ret
755 return ret
756
757
qyearsley53f48a12016-09-01 10:45:13 -0700758def write_try_results_json(output_file, builds):
759 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
760
761 The input |builds| dict is assumed to be generated by Buildbucket.
762 Buildbucket documentation: http://goo.gl/G0s101
763 """
764
765 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800766 """Extracts some of the information from one build dict."""
767 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700768 return {
769 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700770 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800771 'builder_name': parameters.get('builder_name'),
772 'created_ts': build.get('created_ts'),
773 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700774 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800775 'result': build.get('result'),
776 'status': build.get('status'),
777 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700778 'url': build.get('url'),
779 }
780
781 converted = []
782 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000783 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700784 write_json(output_file, converted)
785
786
Aaron Gable13101a62018-02-09 13:20:41 -0800787def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000788 """Prints statistics about the change to the user."""
789 # --no-ext-diff is broken in some versions of Git, so try to work around
790 # this by overriding the environment (but there is still a problem if the
791 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000793 if 'GIT_EXTERNAL_DIFF' in env:
794 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000795
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000796 try:
797 stdout = sys.stdout.fileno()
798 except AttributeError:
799 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000800 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800801 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000802 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000803
804
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000805class BuildbucketResponseException(Exception):
806 pass
807
808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809class Settings(object):
810 def __init__(self):
811 self.default_server = None
812 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000813 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 self.tree_status_url = None
815 self.viewvc_url = None
816 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000817 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000818 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000819 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000820 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000821 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000822 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823
824 def LazyUpdateIfNeeded(self):
825 """Updates the settings from a codereview.settings file, if available."""
826 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000827 # The only value that actually changes the behavior is
828 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000829 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000830 error_ok=True
831 ).strip().lower()
832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000834 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835 LoadCodereviewSettingsFromFile(cr_settings_file)
836 self.updated = True
837
838 def GetDefaultServerUrl(self, error_ok=False):
839 if not self.default_server:
840 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000841 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 if error_ok:
844 return self.default_server
845 if not self.default_server:
846 error_message = ('Could not find settings file. You must configure '
847 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000849 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850 return self.default_server
851
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000852 @staticmethod
853 def GetRelativeRoot():
854 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000855
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000857 if self.root is None:
858 self.root = os.path.abspath(self.GetRelativeRoot())
859 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000861 def GetGitMirror(self, remote='origin'):
862 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000863 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000864 if not os.path.isdir(local_url):
865 return None
866 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
867 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100868 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100869 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000870 if mirror.exists():
871 return mirror
872 return None
873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 def GetTreeStatusUrl(self, error_ok=False):
875 if not self.tree_status_url:
876 error_message = ('You must configure your tree status URL by running '
877 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000878 self.tree_status_url = self._GetRietveldConfig(
879 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 return self.tree_status_url
881
882 def GetViewVCUrl(self):
883 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000884 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 return self.viewvc_url
886
rmistry@google.com90752582014-01-14 21:04:50 +0000887 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000888 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000889
rmistry@google.com78948ed2015-07-08 23:09:57 +0000890 def GetIsSkipDependencyUpload(self, branch_name):
891 """Returns true if specified branch should skip dep uploads."""
892 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
893 error_ok=True)
894
rmistry@google.com5626a922015-02-26 14:03:30 +0000895 def GetRunPostUploadHook(self):
896 run_post_upload_hook = self._GetRietveldConfig(
897 'run-post-upload-hook', error_ok=True)
898 return run_post_upload_hook == "True"
899
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000900 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000901 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000902
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000903 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000904 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000905
ukai@chromium.orge8077812012-02-03 03:41:46 +0000906 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700907 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000908 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700909 self.is_gerrit = (
910 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000911 return self.is_gerrit
912
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000913 def GetSquashGerritUploads(self):
914 """Return true if uploads to Gerrit should be squashed by default."""
915 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700916 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
917 if self.squash_gerrit_uploads is None:
918 # Default is squash now (http://crbug.com/611892#c23).
919 self.squash_gerrit_uploads = not (
920 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
921 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000922 return self.squash_gerrit_uploads
923
tandriia60502f2016-06-20 02:01:53 -0700924 def GetSquashGerritUploadsOverride(self):
925 """Return True or False if codereview.settings should be overridden.
926
927 Returns None if no override has been defined.
928 """
929 # See also http://crbug.com/611892#c23
930 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
931 error_ok=True).strip()
932 if result == 'true':
933 return True
934 if result == 'false':
935 return False
936 return None
937
tandrii@chromium.org28253532016-04-14 13:46:56 +0000938 def GetGerritSkipEnsureAuthenticated(self):
939 """Return True if EnsureAuthenticated should not be done for Gerrit
940 uploads."""
941 if self.gerrit_skip_ensure_authenticated is None:
942 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000943 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000944 error_ok=True).strip() == 'true')
945 return self.gerrit_skip_ensure_authenticated
946
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000947 def GetGitEditor(self):
948 """Return the editor specified in the git config, or None if none is."""
949 if self.git_editor is None:
950 self.git_editor = self._GetConfig('core.editor', error_ok=True)
951 return self.git_editor or None
952
thestig@chromium.org44202a22014-03-11 19:22:18 +0000953 def GetLintRegex(self):
954 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
955 DEFAULT_LINT_REGEX)
956
957 def GetLintIgnoreRegex(self):
958 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
959 DEFAULT_LINT_IGNORE_REGEX)
960
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000961 def GetProject(self):
962 if not self.project:
963 self.project = self._GetRietveldConfig('project', error_ok=True)
964 return self.project
965
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000966 def _GetRietveldConfig(self, param, **kwargs):
967 return self._GetConfig('rietveld.' + param, **kwargs)
968
rmistry@google.com78948ed2015-07-08 23:09:57 +0000969 def _GetBranchConfig(self, branch_name, param, **kwargs):
970 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 def _GetConfig(self, param, **kwargs):
973 self.LazyUpdateIfNeeded()
974 return RunGit(['config', param], **kwargs).strip()
975
976
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100977@contextlib.contextmanager
978def _get_gerrit_project_config_file(remote_url):
979 """Context manager to fetch and store Gerrit's project.config from
980 refs/meta/config branch and store it in temp file.
981
982 Provides a temporary filename or None if there was error.
983 """
984 error, _ = RunGitWithCode([
985 'fetch', remote_url,
986 '+refs/meta/config:refs/git_cl/meta/config'])
987 if error:
988 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700989 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100990 (remote_url, error))
991 yield None
992 return
993
994 error, project_config_data = RunGitWithCode(
995 ['show', 'refs/git_cl/meta/config:project.config'])
996 if error:
997 print('WARNING: project.config file not found')
998 yield None
999 return
1000
1001 with gclient_utils.temporary_directory() as tempdir:
1002 project_config_file = os.path.join(tempdir, 'project.config')
1003 gclient_utils.FileWrite(project_config_file, project_config_data)
1004 yield project_config_file
1005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007def ShortBranchName(branch):
1008 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 return branch.replace('refs/heads/', '', 1)
1010
1011
1012def GetCurrentBranchRef():
1013 """Returns branch ref (e.g., refs/heads/master) or None."""
1014 return RunGit(['symbolic-ref', 'HEAD'],
1015 stderr=subprocess2.VOID, error_ok=True).strip() or None
1016
1017
1018def GetCurrentBranch():
1019 """Returns current branch or None.
1020
1021 For refs/heads/* branches, returns just last part. For others, full ref.
1022 """
1023 branchref = GetCurrentBranchRef()
1024 if branchref:
1025 return ShortBranchName(branchref)
1026 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027
1028
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001029class _CQState(object):
1030 """Enum for states of CL with respect to Commit Queue."""
1031 NONE = 'none'
1032 DRY_RUN = 'dry_run'
1033 COMMIT = 'commit'
1034
1035 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1036
1037
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001039 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001040 self.issue = issue
1041 self.patchset = patchset
1042 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001043 assert codereview in (None, 'rietveld', 'gerrit')
1044 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045
1046 @property
1047 def valid(self):
1048 return self.issue is not None
1049
1050
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001051def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001052 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1053 fail_result = _ParsedIssueNumberArgument()
1054
1055 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001056 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001057 if not arg.startswith('http'):
1058 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001059
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060 url = gclient_utils.UpgradeToHttps(arg)
1061 try:
1062 parsed_url = urlparse.urlparse(url)
1063 except ValueError:
1064 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001065
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001066 if codereview is not None:
1067 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1068 return parsed or fail_result
1069
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001070 results = {}
1071 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1072 parsed = cls.ParseIssueURL(parsed_url)
1073 if parsed is not None:
1074 results[name] = parsed
1075
1076 if not results:
1077 return fail_result
1078 if len(results) == 1:
1079 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001080
1081 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1082 # This is likely Gerrit.
1083 return results['gerrit']
1084 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001085 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001086
1087
Aaron Gablea45ee112016-11-22 15:14:38 -08001088class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001089 def __init__(self, issue, url):
1090 self.issue = issue
1091 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001092 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001093
1094 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001095 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001096 self.issue, self.url)
1097
1098
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001099_CommentSummary = collections.namedtuple(
1100 '_CommentSummary', ['date', 'message', 'sender',
1101 # TODO(tandrii): these two aren't known in Gerrit.
1102 'approval', 'disapproval'])
1103
1104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106 """Changelist works with one changelist in local branch.
1107
1108 Supports two codereview backends: Rietveld or Gerrit, selected at object
1109 creation.
1110
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001111 Notes:
1112 * Not safe for concurrent multi-{thread,process} use.
1113 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001114 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 """
1116
1117 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1118 """Create a new ChangeList instance.
1119
1120 If issue is given, the codereview must be given too.
1121
1122 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1123 Otherwise, it's decided based on current configuration of the local branch,
1124 with default being 'rietveld' for backwards compatibility.
1125 See _load_codereview_impl for more details.
1126
1127 **kwargs will be passed directly to codereview implementation.
1128 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001130 global settings
1131 if not settings:
1132 # Happens when git_cl.py is used as a utility library.
1133 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001134
1135 if issue:
1136 assert codereview, 'codereview must be known, if issue is known'
1137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.branchref = branchref
1139 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001140 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 self.branch = ShortBranchName(self.branchref)
1142 else:
1143 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001145 self.lookedup_issue = False
1146 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 self.has_description = False
1148 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001151 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001152 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001153 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001154 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001155
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001157 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001158 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001159 assert self._codereview_impl
1160 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161
1162 def _load_codereview_impl(self, codereview=None, **kwargs):
1163 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001164 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1165 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1166 self._codereview = codereview
1167 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001168 return
1169
1170 # Automatic selection based on issue number set for a current branch.
1171 # Rietveld takes precedence over Gerrit.
1172 assert not self.issue
1173 # Whether we find issue or not, we are doing the lookup.
1174 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001175 if self.GetBranch():
1176 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1177 issue = _git_get_branch_config_value(
1178 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1179 if issue:
1180 self._codereview = codereview
1181 self._codereview_impl = cls(self, **kwargs)
1182 self.issue = int(issue)
1183 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184
1185 # No issue is set for this branch, so decide based on repo-wide settings.
1186 return self._load_codereview_impl(
1187 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1188 **kwargs)
1189
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001190 def IsGerrit(self):
1191 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192
1193 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001194 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001195
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001196 The return value is a string suitable for passing to git cl with the --cc
1197 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 """
1199 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001200 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001201 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001202 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1203 return self.cc
1204
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001205 def GetCCListWithoutDefault(self):
1206 """Return the users cc'd on this CL excluding default ones."""
1207 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001208 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001209 return self.cc
1210
Daniel Cheng7227d212017-11-17 08:12:37 -08001211 def ExtendCC(self, more_cc):
1212 """Extends the list of users to cc on this CL based on the changed files."""
1213 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214
1215 def GetBranch(self):
1216 """Returns the short branch name, e.g. 'master'."""
1217 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001218 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001219 if not branchref:
1220 return None
1221 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 self.branch = ShortBranchName(self.branchref)
1223 return self.branch
1224
1225 def GetBranchRef(self):
1226 """Returns the full branch name, e.g. 'refs/heads/master'."""
1227 self.GetBranch() # Poke the lazy loader.
1228 return self.branchref
1229
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001230 def ClearBranch(self):
1231 """Clears cached branch data of this object."""
1232 self.branch = self.branchref = None
1233
tandrii5d48c322016-08-18 16:19:37 -07001234 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1235 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1236 kwargs['branch'] = self.GetBranch()
1237 return _git_get_branch_config_value(key, default, **kwargs)
1238
1239 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1240 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1241 assert self.GetBranch(), (
1242 'this CL must have an associated branch to %sset %s%s' %
1243 ('un' if value is None else '',
1244 key,
1245 '' if value is None else ' to %r' % value))
1246 kwargs['branch'] = self.GetBranch()
1247 return _git_set_branch_config_value(key, value, **kwargs)
1248
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 @staticmethod
1250 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001251 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 e.g. 'origin', 'refs/heads/master'
1253 """
1254 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001255 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001258 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001260 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1261 error_ok=True).strip()
1262 if upstream_branch:
1263 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001265 # Else, try to guess the origin remote.
1266 remote_branches = RunGit(['branch', '-r']).split()
1267 if 'origin/master' in remote_branches:
1268 # Fall back on origin/master if it exits.
1269 remote = 'origin'
1270 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001272 DieWithError(
1273 'Unable to determine default branch to diff against.\n'
1274 'Either pass complete "git diff"-style arguments, like\n'
1275 ' git cl upload origin/master\n'
1276 'or verify this branch is set up to track another \n'
1277 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278
1279 return remote, upstream_branch
1280
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001281 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001282 upstream_branch = self.GetUpstreamBranch()
1283 if not BranchExists(upstream_branch):
1284 DieWithError('The upstream for the current branch (%s) does not exist '
1285 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001286 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001287 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 def GetUpstreamBranch(self):
1290 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001291 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001293 upstream_branch = upstream_branch.replace('refs/heads/',
1294 'refs/remotes/%s/' % remote)
1295 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1296 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297 self.upstream_branch = upstream_branch
1298 return self.upstream_branch
1299
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 remote, branch = None, self.GetBranch()
1303 seen_branches = set()
1304 while branch not in seen_branches:
1305 seen_branches.add(branch)
1306 remote, branch = self.FetchUpstreamTuple(branch)
1307 branch = ShortBranchName(branch)
1308 if remote != '.' or branch.startswith('refs/remotes'):
1309 break
1310 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001311 remotes = RunGit(['remote'], error_ok=True).split()
1312 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001315 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001316 logging.warn('Could not determine which remote this change is '
1317 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001318 else:
1319 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001320 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 branch = 'HEAD'
1322 if branch.startswith('refs/remotes'):
1323 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001324 elif branch.startswith('refs/branch-heads/'):
1325 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 else:
1327 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001328 return self._remote
1329
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001330 def GitSanityChecks(self, upstream_git_obj):
1331 """Checks git repo status and ensures diff is from local commits."""
1332
sbc@chromium.org79706062015-01-14 21:18:12 +00001333 if upstream_git_obj is None:
1334 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001335 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001336 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001337 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001338 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001339 return False
1340
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001341 # Verify the commit we're diffing against is in our current branch.
1342 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1343 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1344 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001345 print('ERROR: %s is not in the current branch. You may need to rebase '
1346 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001347 return False
1348
1349 # List the commits inside the diff, and verify they are all local.
1350 commits_in_diff = RunGit(
1351 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1352 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1353 remote_branch = remote_branch.strip()
1354 if code != 0:
1355 _, remote_branch = self.GetRemoteBranch()
1356
1357 commits_in_remote = RunGit(
1358 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1359
1360 common_commits = set(commits_in_diff) & set(commits_in_remote)
1361 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001362 print('ERROR: Your diff contains %d commits already in %s.\n'
1363 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1364 'the diff. If you are using a custom git flow, you can override'
1365 ' the reference used for this check with "git config '
1366 'gitcl.remotebranch <git-ref>".' % (
1367 len(common_commits), remote_branch, upstream_git_obj),
1368 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001369 return False
1370 return True
1371
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001372 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001373 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001374
1375 Returns None if it is not set.
1376 """
tandrii5d48c322016-08-18 16:19:37 -07001377 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 def GetRemoteUrl(self):
1380 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1381
1382 Returns None if there is no remote.
1383 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001384 is_cached, value = self._cached_remote_url
1385 if is_cached:
1386 return value
1387
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001388 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001389 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1390
1391 # If URL is pointing to a local directory, it is probably a git cache.
1392 if os.path.isdir(url):
1393 url = RunGit(['config', 'remote.%s.url' % remote],
1394 error_ok=True,
1395 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001396 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001397 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001399 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001400 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001401 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001402 self.issue = self._GitGetBranchConfigValue(
1403 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001404 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 return self.issue
1406
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 def GetIssueURL(self):
1408 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001409 issue = self.GetIssue()
1410 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001411 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001412 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001414 def GetDescription(self, pretty=False, force=False):
1415 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001417 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 self.has_description = True
1419 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001420 # Set width to 72 columns + 2 space indent.
1421 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001423 lines = self.description.splitlines()
1424 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 return self.description
1426
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001427 def GetDescriptionFooters(self):
1428 """Returns (non_footer_lines, footers) for the commit message.
1429
1430 Returns:
1431 non_footer_lines (list(str)) - Simple list of description lines without
1432 any footer. The lines do not contain newlines, nor does the list contain
1433 the empty line between the message and the footers.
1434 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1435 [("Change-Id", "Ideadbeef...."), ...]
1436 """
1437 raw_description = self.GetDescription()
1438 msg_lines, _, footers = git_footers.split_footers(raw_description)
1439 if footers:
1440 msg_lines = msg_lines[:len(msg_lines)-1]
1441 return msg_lines, footers
1442
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001444 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001446 self.patchset = self._GitGetBranchConfigValue(
1447 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001448 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 return self.patchset
1450
1451 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001452 """Set this branch's patchset. If patchset=0, clears the patchset."""
1453 assert self.GetBranch()
1454 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001455 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001456 else:
1457 self.patchset = int(patchset)
1458 self._GitSetBranchConfigValue(
1459 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001461 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001462 """Set this branch's issue. If issue isn't given, clears the issue."""
1463 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001465 issue = int(issue)
1466 self._GitSetBranchConfigValue(
1467 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001468 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001469 codereview_server = self._codereview_impl.GetCodereviewServer()
1470 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001471 self._GitSetBranchConfigValue(
1472 self._codereview_impl.CodereviewServerConfigKey(),
1473 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 else:
tandrii5d48c322016-08-18 16:19:37 -07001475 # Reset all of these just to be clean.
1476 reset_suffixes = [
1477 'last-upload-hash',
1478 self._codereview_impl.IssueConfigKey(),
1479 self._codereview_impl.PatchsetConfigKey(),
1480 self._codereview_impl.CodereviewServerConfigKey(),
1481 ] + self._PostUnsetIssueProperties()
1482 for prop in reset_suffixes:
1483 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001484 msg = RunGit(['log', '-1', '--format=%B']).strip()
1485 if msg and git_footers.get_footer_change_id(msg):
1486 print('WARNING: The change patched into this branch has a Change-Id. '
1487 'Removing it.')
1488 RunGit(['commit', '--amend', '-m',
1489 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001490 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001491 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492
dnjba1b0f32016-09-02 12:37:42 -07001493 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001494 if not self.GitSanityChecks(upstream_branch):
1495 DieWithError('\nGit sanity check failure')
1496
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001497 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001498 if not root:
1499 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001500 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001501
1502 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001503 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001504 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001505 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001506 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001507 except subprocess2.CalledProcessError:
1508 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001509 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001510 'This branch probably doesn\'t exist anymore. To reset the\n'
1511 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001512 ' git branch --set-upstream-to origin/master %s\n'
1513 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001514 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001515
maruel@chromium.org52424302012-08-29 15:14:30 +00001516 issue = self.GetIssue()
1517 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001518 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519 description = self.GetDescription()
1520 else:
1521 # If the change was never uploaded, use the log messages of all commits
1522 # up to the branch point, as git cl upload will prefill the description
1523 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001524 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1525 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001526
1527 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001528 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001529 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001530 name,
1531 description,
1532 absroot,
1533 files,
1534 issue,
1535 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001536 author,
1537 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001538
dsansomee2d6fd92016-09-08 00:10:47 -07001539 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001540 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001541 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001542 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001543
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001544 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1545 """Sets the description for this CL remotely.
1546
1547 You can get description_lines and footers with GetDescriptionFooters.
1548
1549 Args:
1550 description_lines (list(str)) - List of CL description lines without
1551 newline characters.
1552 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1553 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1554 `List-Of-Tokens`). It will be case-normalized so that each token is
1555 title-cased.
1556 """
1557 new_description = '\n'.join(description_lines)
1558 if footers:
1559 new_description += '\n'
1560 for k, v in footers:
1561 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1562 if not git_footers.FOOTER_PATTERN.match(foot):
1563 raise ValueError('Invalid footer %r' % foot)
1564 new_description += foot + '\n'
1565 self.UpdateDescription(new_description, force)
1566
Edward Lesmes8e282792018-04-03 18:50:29 -04001567 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001568 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1569 try:
1570 return presubmit_support.DoPresubmitChecks(change, committing,
1571 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1572 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001573 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1574 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001575 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001576 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001577
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001578 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1579 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001580 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1581 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001582 else:
1583 # Assume url.
1584 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1585 urlparse.urlparse(issue_arg))
1586 if not parsed_issue_arg or not parsed_issue_arg.valid:
1587 DieWithError('Failed to parse issue argument "%s". '
1588 'Must be an issue number or a valid URL.' % issue_arg)
1589 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001590 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001591
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592 def CMDUpload(self, options, git_diff_args, orig_args):
1593 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001594 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001595 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001596 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001597 else:
1598 if self.GetBranch() is None:
1599 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1600
1601 # Default to diffing against common ancestor of upstream branch
1602 base_branch = self.GetCommonAncestorWithUpstream()
1603 git_diff_args = [base_branch, 'HEAD']
1604
Aaron Gablec4c40d12017-05-22 11:49:53 -07001605 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1606 if not self.IsGerrit() and not self.GetIssue():
1607 print('=====================================')
1608 print('NOTICE: Rietveld is being deprecated. '
1609 'You can upload changes to Gerrit with')
1610 print(' git cl upload --gerrit')
1611 print('or set Gerrit to be your default code review tool with')
1612 print(' git config gerrit.host true')
1613 print('=====================================')
1614
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001615 # Fast best-effort checks to abort before running potentially
1616 # expensive hooks if uploading is likely to fail anyway. Passing these
1617 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001618 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001619 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001620
1621 # Apply watchlists on upload.
1622 change = self.GetChange(base_branch, None)
1623 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1624 files = [f.LocalPath() for f in change.AffectedFiles()]
1625 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001626 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627
1628 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001629 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001630 # Set the reviewer list now so that presubmit checks can access it.
1631 change_description = ChangeDescription(change.FullDescriptionText())
1632 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001633 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001634 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001635 change)
1636 change.SetDescriptionText(change_description.description)
1637 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001638 may_prompt=not options.force,
1639 verbose=options.verbose,
1640 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001641 if not hook_results.should_continue():
1642 return 1
1643 if not options.reviewers and hook_results.reviewers:
1644 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001645 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001647 # TODO(tandrii): Checking local patchset against remote patchset is only
1648 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1649 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001650 latest_patchset = self.GetMostRecentPatchset()
1651 local_patchset = self.GetPatchset()
1652 if (latest_patchset and local_patchset and
1653 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001654 print('The last upload made from this repository was patchset #%d but '
1655 'the most recent patchset on the server is #%d.'
1656 % (local_patchset, latest_patchset))
1657 print('Uploading will still work, but if you\'ve uploaded to this '
1658 'issue from another machine or branch the patch you\'re '
1659 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001660 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001661
Aaron Gable13101a62018-02-09 13:20:41 -08001662 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001663 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001664 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001665 if self.IsGerrit():
1666 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1667 options.cq_dry_run);
1668 else:
1669 if options.use_commit_queue:
1670 self.SetCQState(_CQState.COMMIT)
1671 elif options.cq_dry_run:
1672 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001673
tandrii5d48c322016-08-18 16:19:37 -07001674 _git_set_branch_config_value('last-upload-hash',
1675 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001676 # Run post upload hooks, if specified.
1677 if settings.GetRunPostUploadHook():
1678 presubmit_support.DoPostUploadExecuter(
1679 change,
1680 self,
1681 settings.GetRoot(),
1682 options.verbose,
1683 sys.stdout)
1684
1685 # Upload all dependencies if specified.
1686 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001687 print()
1688 print('--dependencies has been specified.')
1689 print('All dependent local branches will be re-uploaded.')
1690 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001691 # Remove the dependencies flag from args so that we do not end up in a
1692 # loop.
1693 orig_args.remove('--dependencies')
1694 ret = upload_branch_deps(self, orig_args)
1695 return ret
1696
Ravi Mistry31e7d562018-04-02 12:53:57 -04001697 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1698 """Sets labels on the change based on the provided flags.
1699
1700 Sets labels if issue is already uploaded and known, else returns without
1701 doing anything.
1702
1703 Args:
1704 enable_auto_submit: Sets Auto-Submit+1 on the change.
1705 use_commit_queue: Sets Commit-Queue+2 on the change.
1706 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1707 both use_commit_queue and cq_dry_run are true.
1708 """
1709 if not self.GetIssue():
1710 return
1711 try:
1712 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1713 cq_dry_run)
1714 return 0
1715 except KeyboardInterrupt:
1716 raise
1717 except:
1718 labels = []
1719 if enable_auto_submit:
1720 labels.append('Auto-Submit')
1721 if use_commit_queue or cq_dry_run:
1722 labels.append('Commit-Queue')
1723 print('WARNING: Failed to set label(s) on your change: %s\n'
1724 'Either:\n'
1725 ' * Your project does not have the above label(s),\n'
1726 ' * You don\'t have permission to set the above label(s),\n'
1727 ' * There\'s a bug in this code (see stack trace below).\n' %
1728 (', '.join(labels)))
1729 # Still raise exception so that stack trace is printed.
1730 raise
1731
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001732 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001733 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001734
1735 Issue must have been already uploaded and known.
1736 """
1737 assert new_state in _CQState.ALL_STATES
1738 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001739 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001740 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001741 return 0
1742 except KeyboardInterrupt:
1743 raise
1744 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001745 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001746 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001747 ' * Your project has no CQ,\n'
1748 ' * You don\'t have permission to change the CQ state,\n'
1749 ' * There\'s a bug in this code (see stack trace below).\n'
1750 'Consider specifying which bots to trigger manually or asking your '
1751 'project owners for permissions or contacting Chrome Infra at:\n'
1752 'https://www.chromium.org/infra\n\n' %
1753 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001754 # Still raise exception so that stack trace is printed.
1755 raise
1756
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001757 # Forward methods to codereview specific implementation.
1758
Aaron Gable636b13f2017-07-14 10:42:48 -07001759 def AddComment(self, message, publish=None):
1760 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001761
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001762 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001763 """Returns list of _CommentSummary for each comment.
1764
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001765 args:
1766 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001767 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001768 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001769
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001770 def CloseIssue(self):
1771 return self._codereview_impl.CloseIssue()
1772
1773 def GetStatus(self):
1774 return self._codereview_impl.GetStatus()
1775
1776 def GetCodereviewServer(self):
1777 return self._codereview_impl.GetCodereviewServer()
1778
tandriide281ae2016-10-12 06:02:30 -07001779 def GetIssueOwner(self):
1780 """Get owner from codereview, which may differ from this checkout."""
1781 return self._codereview_impl.GetIssueOwner()
1782
Edward Lemur707d70b2018-02-07 00:50:14 +01001783 def GetReviewers(self):
1784 return self._codereview_impl.GetReviewers()
1785
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001786 def GetMostRecentPatchset(self):
1787 return self._codereview_impl.GetMostRecentPatchset()
1788
tandriide281ae2016-10-12 06:02:30 -07001789 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001790 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001791 return self._codereview_impl.CannotTriggerTryJobReason()
1792
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001793 def GetTryJobProperties(self, patchset=None):
1794 """Returns dictionary of properties to launch try job."""
1795 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001796
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 def __getattr__(self, attr):
1798 # This is because lots of untested code accesses Rietveld-specific stuff
1799 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001800 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001801 # Note that child method defines __getattr__ as well, and forwards it here,
1802 # because _RietveldChangelistImpl is not cleaned up yet, and given
1803 # deprecation of Rietveld, it should probably be just removed.
1804 # Until that time, avoid infinite recursion by bypassing __getattr__
1805 # of implementation class.
1806 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807
1808
1809class _ChangelistCodereviewBase(object):
1810 """Abstract base class encapsulating codereview specifics of a changelist."""
1811 def __init__(self, changelist):
1812 self._changelist = changelist # instance of Changelist
1813
1814 def __getattr__(self, attr):
1815 # Forward methods to changelist.
1816 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1817 # _RietveldChangelistImpl to avoid this hack?
1818 return getattr(self._changelist, attr)
1819
1820 def GetStatus(self):
1821 """Apply a rough heuristic to give a simple summary of an issue's review
1822 or CQ status, assuming adherence to a common workflow.
1823
1824 Returns None if no issue for this branch, or specific string keywords.
1825 """
1826 raise NotImplementedError()
1827
1828 def GetCodereviewServer(self):
1829 """Returns server URL without end slash, like "https://codereview.com"."""
1830 raise NotImplementedError()
1831
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001832 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001833 """Fetches and returns description from the codereview server."""
1834 raise NotImplementedError()
1835
tandrii5d48c322016-08-18 16:19:37 -07001836 @classmethod
1837 def IssueConfigKey(cls):
1838 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001839 raise NotImplementedError()
1840
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001841 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001842 def PatchsetConfigKey(cls):
1843 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844 raise NotImplementedError()
1845
tandrii5d48c322016-08-18 16:19:37 -07001846 @classmethod
1847 def CodereviewServerConfigKey(cls):
1848 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 raise NotImplementedError()
1850
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001851 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001852 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001853 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001854
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001855 def GetGerritObjForPresubmit(self):
1856 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1857 return None
1858
dsansomee2d6fd92016-09-08 00:10:47 -07001859 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001860 """Update the description on codereview site."""
1861 raise NotImplementedError()
1862
Aaron Gable636b13f2017-07-14 10:42:48 -07001863 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001864 """Posts a comment to the codereview site."""
1865 raise NotImplementedError()
1866
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001867 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001868 raise NotImplementedError()
1869
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001870 def CloseIssue(self):
1871 """Closes the issue."""
1872 raise NotImplementedError()
1873
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001874 def GetMostRecentPatchset(self):
1875 """Returns the most recent patchset number from the codereview site."""
1876 raise NotImplementedError()
1877
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001878 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001879 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001880 """Fetches and applies the issue.
1881
1882 Arguments:
1883 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1884 reject: if True, reject the failed patch instead of switching to 3-way
1885 merge. Rietveld only.
1886 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1887 only.
1888 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001889 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001890 """
1891 raise NotImplementedError()
1892
1893 @staticmethod
1894 def ParseIssueURL(parsed_url):
1895 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1896 failed."""
1897 raise NotImplementedError()
1898
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001899 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001900 """Best effort check that user is authenticated with codereview server.
1901
1902 Arguments:
1903 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001904 refresh: whether to attempt to refresh credentials. Ignored if not
1905 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001906 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001907 raise NotImplementedError()
1908
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001909 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001910 """Best effort check that uploading isn't supposed to fail for predictable
1911 reasons.
1912
1913 This method should raise informative exception if uploading shouldn't
1914 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001915
1916 Arguments:
1917 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001918 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001919 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001920
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001921 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001922 """Uploads a change to codereview."""
1923 raise NotImplementedError()
1924
Ravi Mistry31e7d562018-04-02 12:53:57 -04001925 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1926 """Sets labels on the change based on the provided flags.
1927
1928 Issue must have been already uploaded and known.
1929 """
1930 raise NotImplementedError()
1931
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001932 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001933 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001934
1935 Issue must have been already uploaded and known.
1936 """
1937 raise NotImplementedError()
1938
tandriie113dfd2016-10-11 10:20:12 -07001939 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001940 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001941 raise NotImplementedError()
1942
tandriide281ae2016-10-12 06:02:30 -07001943 def GetIssueOwner(self):
1944 raise NotImplementedError()
1945
Edward Lemur707d70b2018-02-07 00:50:14 +01001946 def GetReviewers(self):
1947 raise NotImplementedError()
1948
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001949 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001950 raise NotImplementedError()
1951
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001952
1953class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001954
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001955 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 super(_RietveldChangelistImpl, self).__init__(changelist)
1957 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001958 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001959 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001961 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001962 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001963 self._props = None
1964 self._rpc_server = None
1965
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001966 def GetCodereviewServer(self):
1967 if not self._rietveld_server:
1968 # If we're on a branch then get the server potentially associated
1969 # with that branch.
1970 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001971 self._rietveld_server = gclient_utils.UpgradeToHttps(
1972 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 if not self._rietveld_server:
1974 self._rietveld_server = settings.GetDefaultServerUrl()
1975 return self._rietveld_server
1976
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001977 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001978 """Best effort check that user is authenticated with Rietveld server."""
1979 if self._auth_config.use_oauth2:
1980 authenticator = auth.get_authenticator_for_host(
1981 self.GetCodereviewServer(), self._auth_config)
1982 if not authenticator.has_cached_credentials():
1983 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001984 if refresh:
1985 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001986
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001987 def EnsureCanUploadPatchset(self, force):
1988 # No checks for Rietveld because we are deprecating Rietveld.
1989 pass
1990
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001991 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001992 issue = self.GetIssue()
1993 assert issue
1994 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001995 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001996 except urllib2.HTTPError as e:
1997 if e.code == 404:
1998 DieWithError(
1999 ('\nWhile fetching the description for issue %d, received a '
2000 '404 (not found)\n'
2001 'error. It is likely that you deleted this '
2002 'issue on the server. If this is the\n'
2003 'case, please run\n\n'
2004 ' git cl issue 0\n\n'
2005 'to clear the association with the deleted issue. Then run '
2006 'this command again.') % issue)
2007 else:
2008 DieWithError(
2009 '\nFailed to fetch issue description. HTTP error %d' % e.code)
2010 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07002011 print('Warning: Failed to retrieve CL description due to network '
2012 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002013 return ''
2014
2015 def GetMostRecentPatchset(self):
2016 return self.GetIssueProperties()['patchsets'][-1]
2017
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002018 def GetIssueProperties(self):
2019 if self._props is None:
2020 issue = self.GetIssue()
2021 if not issue:
2022 self._props = {}
2023 else:
2024 self._props = self.RpcServer().get_issue_properties(issue, True)
2025 return self._props
2026
tandriie113dfd2016-10-11 10:20:12 -07002027 def CannotTriggerTryJobReason(self):
2028 props = self.GetIssueProperties()
2029 if not props:
2030 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
2031 if props.get('closed'):
2032 return 'CL %s is closed' % self.GetIssue()
2033 if props.get('private'):
2034 return 'CL %s is private' % self.GetIssue()
2035 return None
2036
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002037 def GetTryJobProperties(self, patchset=None):
2038 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002039 project = (self.GetIssueProperties() or {}).get('project')
2040 return {
2041 'issue': self.GetIssue(),
2042 'patch_project': project,
2043 'patch_storage': 'rietveld',
2044 'patchset': patchset or self.GetPatchset(),
2045 'rietveld': self.GetCodereviewServer(),
2046 }
2047
tandriide281ae2016-10-12 06:02:30 -07002048 def GetIssueOwner(self):
2049 return (self.GetIssueProperties() or {}).get('owner_email')
2050
Edward Lemur707d70b2018-02-07 00:50:14 +01002051 def GetReviewers(self):
2052 return (self.GetIssueProperties() or {}).get('reviewers')
2053
Aaron Gable636b13f2017-07-14 10:42:48 -07002054 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002055 return self.RpcServer().add_comment(self.GetIssue(), message)
2056
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002057 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002058 summary = []
2059 for message in self.GetIssueProperties().get('messages', []):
2060 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2061 summary.append(_CommentSummary(
2062 date=date,
2063 disapproval=bool(message['disapproval']),
2064 approval=bool(message['approval']),
2065 sender=message['sender'],
2066 message=message['text'],
2067 ))
2068 return summary
2069
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002070 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002071 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002072 or CQ status, assuming adherence to a common workflow.
2073
2074 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002075 * 'error' - error from review tool (including deleted issues)
2076 * 'unsent' - not sent for review
2077 * 'waiting' - waiting for review
2078 * 'reply' - waiting for owner to reply to review
2079 * 'not lgtm' - Code-Review label has been set negatively
2080 * 'lgtm' - LGTM from at least one approved reviewer
2081 * 'commit' - in the commit queue
2082 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002083 """
2084 if not self.GetIssue():
2085 return None
2086
2087 try:
2088 props = self.GetIssueProperties()
2089 except urllib2.HTTPError:
2090 return 'error'
2091
2092 if props.get('closed'):
2093 # Issue is closed.
2094 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002095 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002096 # Issue is in the commit queue.
2097 return 'commit'
2098
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002099 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002100 if not messages:
2101 # No message was sent.
2102 return 'unsent'
2103
2104 if get_approving_reviewers(props):
2105 return 'lgtm'
2106 elif get_approving_reviewers(props, disapproval=True):
2107 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002108
tandrii9d2c7a32016-06-22 03:42:45 -07002109 # Skip CQ messages that don't require owner's action.
2110 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2111 if 'Dry run:' in messages[-1]['text']:
2112 messages.pop()
2113 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2114 # This message always follows prior messages from CQ,
2115 # so skip this too.
2116 messages.pop()
2117 else:
2118 # This is probably a CQ messages warranting user attention.
2119 break
2120
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002121 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002122 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002123 return 'reply'
2124 return 'waiting'
2125
dsansomee2d6fd92016-09-08 00:10:47 -07002126 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002127 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002130 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002132 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002133 return self.SetFlags({flag: value})
2134
2135 def SetFlags(self, flags):
2136 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002137 """
phajdan.jr68598232016-08-10 03:28:28 -07002138 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002139 try:
tandrii4b233bd2016-07-06 03:50:29 -07002140 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002141 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002142 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002143 if e.code == 404:
2144 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2145 if e.code == 403:
2146 DieWithError(
2147 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002148 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002149 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002151 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 """Returns an upload.RpcServer() to access this review's rietveld instance.
2153 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002154 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002155 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002156 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002157 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002158 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002160 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002161 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002162 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163
tandrii5d48c322016-08-18 16:19:37 -07002164 @classmethod
2165 def PatchsetConfigKey(cls):
2166 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002167
tandrii5d48c322016-08-18 16:19:37 -07002168 @classmethod
2169 def CodereviewServerConfigKey(cls):
2170 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002171
Ravi Mistry31e7d562018-04-02 12:53:57 -04002172 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2173 raise NotImplementedError()
2174
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002175 def SetCQState(self, new_state):
2176 props = self.GetIssueProperties()
2177 if props.get('private'):
2178 DieWithError('Cannot set-commit on private issue')
2179
2180 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002181 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002182 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002183 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002184 else:
tandrii4b233bd2016-07-06 03:50:29 -07002185 assert new_state == _CQState.DRY_RUN
2186 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002187
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002188 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002189 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002190 # PatchIssue should never be called with a dirty tree. It is up to the
2191 # caller to check this, but just in case we assert here since the
2192 # consequences of the caller not checking this could be dire.
2193 assert(not git_common.is_dirty_git_tree('apply'))
2194 assert(parsed_issue_arg.valid)
2195 self._changelist.issue = parsed_issue_arg.issue
2196 if parsed_issue_arg.hostname:
2197 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2198
skobes6468b902016-10-24 08:45:10 -07002199 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2200 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2201 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 try:
skobes6468b902016-10-24 08:45:10 -07002203 scm_obj.apply_patch(patchset_object)
2204 except Exception as e:
2205 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002206 return 1
2207
2208 # If we had an issue, commit the current state and register the issue.
2209 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002210 self.SetIssue(self.GetIssue())
2211 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002212 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2213 'patch from issue %(i)s at patchset '
2214 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2215 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002216 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002217 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002218 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002219 return 0
2220
2221 @staticmethod
2222 def ParseIssueURL(parsed_url):
2223 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2224 return None
wychen3c1c1722016-08-04 11:46:36 -07002225 # Rietveld patch: https://domain/<number>/#ps<patchset>
2226 match = re.match(r'/(\d+)/$', parsed_url.path)
2227 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2228 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002229 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002230 issue=int(match.group(1)),
2231 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002232 hostname=parsed_url.netloc,
2233 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002234 # Typical url: https://domain/<issue_number>[/[other]]
2235 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2236 if match:
skobes6468b902016-10-24 08:45:10 -07002237 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002238 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002239 hostname=parsed_url.netloc,
2240 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002241 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2242 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2243 if match:
skobes6468b902016-10-24 08:45:10 -07002244 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002245 issue=int(match.group(1)),
2246 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002247 hostname=parsed_url.netloc,
2248 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002249 return None
2250
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002251 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002252 """Upload the patch to Rietveld."""
2253 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2254 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002255 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2256 if options.emulate_svn_auto_props:
2257 upload_args.append('--emulate_svn_auto_props')
2258
2259 change_desc = None
2260
2261 if options.email is not None:
2262 upload_args.extend(['--email', options.email])
2263
2264 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002265 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002266 upload_args.extend(['--title', options.title])
2267 if options.message:
2268 upload_args.extend(['--message', options.message])
2269 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002270 print('This branch is associated with issue %s. '
2271 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002272 else:
nodirca166002016-06-27 10:59:51 -07002273 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002274 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002275 if options.message:
2276 message = options.message
2277 else:
2278 message = CreateDescriptionFromLog(args)
2279 if options.title:
2280 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002281 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002282 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002283 change_desc.update_reviewers(options.reviewers, options.tbrs,
2284 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002286 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002287
2288 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002289 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002290 return 1
2291
2292 upload_args.extend(['--message', change_desc.description])
2293 if change_desc.get_reviewers():
2294 upload_args.append('--reviewers=%s' % ','.join(
2295 change_desc.get_reviewers()))
2296 if options.send_mail:
2297 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002298 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002299 upload_args.append('--send_mail')
2300
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00002301 # We only skip auto-CC-ing addresses from rietveld.cc when --private or
2302 # --no-autocc is explicitly specified on the command line. Should private
2303 # CL be created due to rietveld.private value, we assume that rietveld.cc
2304 # only contains addresses where private CLs are allowed to be sent.
2305 if options.private or options.no_autocc:
2306 logging.warn('rietveld.cc is ignored since private/no-autocc flag is '
2307 'specified. You need to review and add them manually if '
2308 'necessary.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002309 cc = self.GetCCListWithoutDefault()
2310 else:
2311 cc = self.GetCCList()
2312 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002313 if change_desc.get_cced():
2314 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002315 if cc:
2316 upload_args.extend(['--cc', cc])
2317
2318 if options.private or settings.GetDefaultPrivateFlag() == "True":
2319 upload_args.append('--private')
2320
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002321 # Include the upstream repo's URL in the change -- this is useful for
2322 # projects that have their source spread across multiple repos.
2323 remote_url = self.GetGitBaseUrlFromConfig()
2324 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002325 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2326 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2327 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002328 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002329 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002330 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002331 if target_ref:
2332 upload_args.extend(['--target_ref', target_ref])
2333
2334 # Look for dependent patchsets. See crbug.com/480453 for more details.
2335 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2336 upstream_branch = ShortBranchName(upstream_branch)
2337 if remote is '.':
2338 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002339 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002340 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002341 print()
2342 print('Skipping dependency patchset upload because git config '
2343 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2344 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002345 else:
2346 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002347 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002348 auth_config=auth_config)
2349 branch_cl_issue_url = branch_cl.GetIssueURL()
2350 branch_cl_issue = branch_cl.GetIssue()
2351 branch_cl_patchset = branch_cl.GetPatchset()
2352 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2353 upload_args.extend(
2354 ['--depends_on_patchset', '%s:%s' % (
2355 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002356 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002357 '\n'
2358 'The current branch (%s) is tracking a local branch (%s) with '
2359 'an associated CL.\n'
2360 'Adding %s/#ps%s as a dependency patchset.\n'
2361 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2362 branch_cl_patchset))
2363
2364 project = settings.GetProject()
2365 if project:
2366 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002367 else:
2368 print()
2369 print('WARNING: Uploading without a project specified. Please ensure '
2370 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2371 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002372
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002373 try:
2374 upload_args = ['upload'] + upload_args + args
2375 logging.info('upload.RealMain(%s)', upload_args)
2376 issue, patchset = upload.RealMain(upload_args)
2377 issue = int(issue)
2378 patchset = int(patchset)
2379 except KeyboardInterrupt:
2380 sys.exit(1)
2381 except:
2382 # If we got an exception after the user typed a description for their
2383 # change, back up the description before re-raising.
2384 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002385 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002386 raise
2387
2388 if not self.GetIssue():
2389 self.SetIssue(issue)
2390 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002391 return 0
2392
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002393
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002394class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002395 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002396 # auth_config is Rietveld thing, kept here to preserve interface only.
2397 super(_GerritChangelistImpl, self).__init__(changelist)
2398 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002399 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002400 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002401 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002402 # Map from change number (issue) to its detail cache.
2403 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002405 if codereview_host is not None:
2406 assert not codereview_host.startswith('https://'), codereview_host
2407 self._gerrit_host = codereview_host
2408 self._gerrit_server = 'https://%s' % codereview_host
2409
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002410 def _GetGerritHost(self):
2411 # Lazy load of configs.
2412 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002413 if self._gerrit_host and '.' not in self._gerrit_host:
2414 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2415 # This happens for internal stuff http://crbug.com/614312.
2416 parsed = urlparse.urlparse(self.GetRemoteUrl())
2417 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002418 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002419 ' Your current remote is: %s' % self.GetRemoteUrl())
2420 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2421 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002422 return self._gerrit_host
2423
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002424 def _GetGitHost(self):
2425 """Returns git host to be used when uploading change to Gerrit."""
2426 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2427
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002428 def GetCodereviewServer(self):
2429 if not self._gerrit_server:
2430 # If we're on a branch then get the server potentially associated
2431 # with that branch.
2432 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002433 self._gerrit_server = self._GitGetBranchConfigValue(
2434 self.CodereviewServerConfigKey())
2435 if self._gerrit_server:
2436 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002437 if not self._gerrit_server:
2438 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2439 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002440 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002441 parts[0] = parts[0] + '-review'
2442 self._gerrit_host = '.'.join(parts)
2443 self._gerrit_server = 'https://%s' % self._gerrit_host
2444 return self._gerrit_server
2445
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002446 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002447 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002448 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002449 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002450 logging.warn('can\'t detect Gerrit project.')
2451 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002452 project = urlparse.urlparse(remote_url).path.strip('/')
2453 if project.endswith('.git'):
2454 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002455 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2456 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2457 # gitiles/git-over-https protocol. E.g.,
2458 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2459 # as
2460 # https://chromium.googlesource.com/v8/v8
2461 if project.startswith('a/'):
2462 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002463 return project
2464
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002465 def _GerritChangeIdentifier(self):
2466 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2467
2468 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002469 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002470 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002471 project = self._GetGerritProject()
2472 if project:
2473 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2474 # Fall back on still unique, but less efficient change number.
2475 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002476
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002477 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002478 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002479 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002480
tandrii5d48c322016-08-18 16:19:37 -07002481 @classmethod
2482 def PatchsetConfigKey(cls):
2483 return 'gerritpatchset'
2484
2485 @classmethod
2486 def CodereviewServerConfigKey(cls):
2487 return 'gerritserver'
2488
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002489 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002490 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002491 if settings.GetGerritSkipEnsureAuthenticated():
2492 # For projects with unusual authentication schemes.
2493 # See http://crbug.com/603378.
2494 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002495
2496 # Check presence of cookies only if using cookies-based auth method.
2497 cookie_auth = gerrit_util.Authenticator.get()
2498 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002499 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002500
2501 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002502 self.GetCodereviewServer()
2503 git_host = self._GetGitHost()
2504 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002505
2506 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2507 git_auth = cookie_auth.get_auth_header(git_host)
2508 if gerrit_auth and git_auth:
2509 if gerrit_auth == git_auth:
2510 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002511 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002512 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002513 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002514 ' %s\n'
2515 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002516 ' Consider running the following command:\n'
2517 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002518 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002519 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002520 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002521 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002522 cookie_auth.get_new_password_message(git_host)))
2523 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002524 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002525 return
2526 else:
2527 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002528 ([] if gerrit_auth else [self._gerrit_host]) +
2529 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002530 DieWithError('Credentials for the following hosts are required:\n'
2531 ' %s\n'
2532 'These are read from %s (or legacy %s)\n'
2533 '%s' % (
2534 '\n '.join(missing),
2535 cookie_auth.get_gitcookies_path(),
2536 cookie_auth.get_netrc_path(),
2537 cookie_auth.get_new_password_message(git_host)))
2538
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002539 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002540 if not self.GetIssue():
2541 return
2542
2543 # Warm change details cache now to avoid RPCs later, reducing latency for
2544 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002545 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002546 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002547
2548 status = self._GetChangeDetail()['status']
2549 if status in ('MERGED', 'ABANDONED'):
2550 DieWithError('Change %s has been %s, new uploads are not allowed' %
2551 (self.GetIssueURL(),
2552 'submitted' if status == 'MERGED' else 'abandoned'))
2553
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002554 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2555 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2556 # Apparently this check is not very important? Otherwise get_auth_email
2557 # could have been added to other implementations of Authenticator.
2558 cookies_auth = gerrit_util.Authenticator.get()
2559 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002560 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002561
2562 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002563 if self.GetIssueOwner() == cookies_user:
2564 return
2565 logging.debug('change %s owner is %s, cookies user is %s',
2566 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002567 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002568 # so ask what Gerrit thinks of this user.
2569 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2570 if details['email'] == self.GetIssueOwner():
2571 return
2572 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002573 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002574 'as %s.\n'
2575 'Uploading may fail due to lack of permissions.' %
2576 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2577 confirm_or_exit(action='upload')
2578
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002579 def _PostUnsetIssueProperties(self):
2580 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002581 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002582
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002583 def GetGerritObjForPresubmit(self):
2584 return presubmit_support.GerritAccessor(self._GetGerritHost())
2585
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002586 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002587 """Apply a rough heuristic to give a simple summary of an issue's review
2588 or CQ status, assuming adherence to a common workflow.
2589
2590 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002591 * 'error' - error from review tool (including deleted issues)
2592 * 'unsent' - no reviewers added
2593 * 'waiting' - waiting for review
2594 * 'reply' - waiting for uploader to reply to review
2595 * 'lgtm' - Code-Review label has been set
2596 * 'commit' - in the commit queue
2597 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002598 """
2599 if not self.GetIssue():
2600 return None
2601
2602 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002603 data = self._GetChangeDetail([
2604 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002605 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002606 return 'error'
2607
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002608 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002609 return 'closed'
2610
Aaron Gable9ab38c62017-04-06 14:36:33 -07002611 if data['labels'].get('Commit-Queue', {}).get('approved'):
2612 # The section will have an "approved" subsection if anyone has voted
2613 # the maximum value on the label.
2614 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002615
Aaron Gable9ab38c62017-04-06 14:36:33 -07002616 if data['labels'].get('Code-Review', {}).get('approved'):
2617 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002618
2619 if not data.get('reviewers', {}).get('REVIEWER', []):
2620 return 'unsent'
2621
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002622 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002623 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2624 last_message_author = messages.pop().get('author', {})
2625 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002626 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2627 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002628 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002629 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002630 if last_message_author.get('_account_id') == owner:
2631 # Most recent message was by owner.
2632 return 'waiting'
2633 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002634 # Some reply from non-owner.
2635 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002636
2637 # Somehow there are no messages even though there are reviewers.
2638 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002639
2640 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002641 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002642 patchset = data['revisions'][data['current_revision']]['_number']
2643 self.SetPatchset(patchset)
2644 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002645
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002646 def FetchDescription(self, force=False):
2647 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2648 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002649 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002650 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002651
dsansomee2d6fd92016-09-08 00:10:47 -07002652 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002653 if gerrit_util.HasPendingChangeEdit(
2654 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002655 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002656 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002657 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002658 'unpublished edit. Either publish the edit in the Gerrit web UI '
2659 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002660
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002661 gerrit_util.DeletePendingChangeEdit(
2662 self._GetGerritHost(), self._GerritChangeIdentifier())
2663 gerrit_util.SetCommitMessage(
2664 self._GetGerritHost(), self._GerritChangeIdentifier(),
2665 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002666
Aaron Gable636b13f2017-07-14 10:42:48 -07002667 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002668 gerrit_util.SetReview(
2669 self._GetGerritHost(), self._GerritChangeIdentifier(),
2670 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002671
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002672 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002673 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002674 messages = self._GetChangeDetail(
2675 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2676 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002677 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002678
2679 # Build dictionary of file comments for easy access and sorting later.
2680 # {author+date: {path: {patchset: {line: url+message}}}}
2681 comments = collections.defaultdict(
2682 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2683 for path, line_comments in file_comments.iteritems():
2684 for comment in line_comments:
2685 if comment.get('tag', '').startswith('autogenerated'):
2686 continue
2687 key = (comment['author']['email'], comment['updated'])
2688 if comment.get('side', 'REVISION') == 'PARENT':
2689 patchset = 'Base'
2690 else:
2691 patchset = 'PS%d' % comment['patch_set']
2692 line = comment.get('line', 0)
2693 url = ('https://%s/c/%s/%s/%s#%s%s' %
2694 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2695 'b' if comment.get('side') == 'PARENT' else '',
2696 str(line) if line else ''))
2697 comments[key][path][patchset][line] = (url, comment['message'])
2698
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002699 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002700 for msg in messages:
2701 # Don't bother showing autogenerated messages.
2702 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2703 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002704 # Gerrit spits out nanoseconds.
2705 assert len(msg['date'].split('.')[-1]) == 9
2706 date = datetime.datetime.strptime(msg['date'][:-3],
2707 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002708 message = msg['message']
2709 key = (msg['author']['email'], msg['date'])
2710 if key in comments:
2711 message += '\n'
2712 for path, patchsets in sorted(comments.get(key, {}).items()):
2713 if readable:
2714 message += '\n%s' % path
2715 for patchset, lines in sorted(patchsets.items()):
2716 for line, (url, content) in sorted(lines.items()):
2717 if line:
2718 line_str = 'Line %d' % line
2719 path_str = '%s:%d:' % (path, line)
2720 else:
2721 line_str = 'File comment'
2722 path_str = '%s:0:' % path
2723 if readable:
2724 message += '\n %s, %s: %s' % (patchset, line_str, url)
2725 message += '\n %s\n' % content
2726 else:
2727 message += '\n%s ' % path_str
2728 message += '\n%s\n' % content
2729
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002730 summary.append(_CommentSummary(
2731 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002732 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002733 sender=msg['author']['email'],
2734 # These could be inferred from the text messages and correlated with
2735 # Code-Review label maximum, however this is not reliable.
2736 # Leaving as is until the need arises.
2737 approval=False,
2738 disapproval=False,
2739 ))
2740 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002742 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002743 gerrit_util.AbandonChange(
2744 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002745
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002746 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002747 gerrit_util.SubmitChange(
2748 self._GetGerritHost(), self._GerritChangeIdentifier(),
2749 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002750
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002751 def _GetChangeDetail(self, options=None, no_cache=False):
2752 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002753
2754 If fresh data is needed, set no_cache=True which will clear cache and
2755 thus new data will be fetched from Gerrit.
2756 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002758 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002759
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002760 # Optimization to avoid multiple RPCs:
2761 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2762 'CURRENT_COMMIT' not in options):
2763 options.append('CURRENT_COMMIT')
2764
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002765 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002766 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002767 options = [o.upper() for o in options]
2768
2769 # Check in cache first unless no_cache is True.
2770 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002771 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002772 else:
2773 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002774 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002775 # Assumption: data fetched before with extra options is suitable
2776 # for return for a smaller set of options.
2777 # For example, if we cached data for
2778 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2779 # and request is for options=[CURRENT_REVISION],
2780 # THEN we can return prior cached data.
2781 if options_set.issubset(cached_options_set):
2782 return data
2783
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002784 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002785 data = gerrit_util.GetChangeDetail(
2786 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002787 except gerrit_util.GerritError as e:
2788 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002789 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002790 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002791
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002792 self._detail_cache.setdefault(cache_key, []).append(
2793 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002794 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002795
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002796 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002797 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002798 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002799 data = gerrit_util.GetChangeCommit(
2800 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002801 except gerrit_util.GerritError as e:
2802 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002803 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002804 raise
agable32978d92016-11-01 12:55:02 -07002805 return data
2806
Olivier Robin75ee7252018-04-13 10:02:56 +02002807 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002808 if git_common.is_dirty_git_tree('land'):
2809 return 1
tandriid60367b2016-06-22 05:25:12 -07002810 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2811 if u'Commit-Queue' in detail.get('labels', {}):
2812 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002813 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2814 'which can test and land changes for you. '
2815 'Are you sure you wish to bypass it?\n',
2816 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002817
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002818 differs = True
tandriic4344b52016-08-29 06:04:54 -07002819 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002820 # Note: git diff outputs nothing if there is no diff.
2821 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002822 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002823 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002824 if detail['current_revision'] == last_upload:
2825 differs = False
2826 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002827 print('WARNING: Local branch contents differ from latest uploaded '
2828 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002829 if differs:
2830 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002831 confirm_or_exit(
2832 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2833 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002834 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002835 elif not bypass_hooks:
2836 hook_results = self.RunHook(
2837 committing=True,
2838 may_prompt=not force,
2839 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002840 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2841 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002842 if not hook_results.should_continue():
2843 return 1
2844
2845 self.SubmitIssue(wait_for_merge=True)
2846 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002847 links = self._GetChangeCommit().get('web_links', [])
2848 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002849 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002850 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002851 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002852 return 0
2853
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002854 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002855 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002856 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002857 assert not directory
2858 assert parsed_issue_arg.valid
2859
2860 self._changelist.issue = parsed_issue_arg.issue
2861
2862 if parsed_issue_arg.hostname:
2863 self._gerrit_host = parsed_issue_arg.hostname
2864 self._gerrit_server = 'https://%s' % self._gerrit_host
2865
tandriic2405f52016-10-10 08:13:15 -07002866 try:
2867 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002868 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002869 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002870
2871 if not parsed_issue_arg.patchset:
2872 # Use current revision by default.
2873 revision_info = detail['revisions'][detail['current_revision']]
2874 patchset = int(revision_info['_number'])
2875 else:
2876 patchset = parsed_issue_arg.patchset
2877 for revision_info in detail['revisions'].itervalues():
2878 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2879 break
2880 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002881 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002882 (parsed_issue_arg.patchset, self.GetIssue()))
2883
Aaron Gable697a91b2018-01-19 15:20:15 -08002884 remote_url = self._changelist.GetRemoteUrl()
2885 if remote_url.endswith('.git'):
2886 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002887 remote_url = remote_url.rstrip('/')
2888
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002889 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002890 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002891
2892 if remote_url != fetch_info['url']:
2893 DieWithError('Trying to patch a change from %s but this repo appears '
2894 'to be %s.' % (fetch_info['url'], remote_url))
2895
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002896 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002897
Aaron Gable62619a32017-06-16 08:22:09 -07002898 if force:
2899 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2900 print('Checked out commit for change %i patchset %i locally' %
2901 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002902 elif nocommit:
2903 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2904 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002905 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002906 RunGit(['cherry-pick', 'FETCH_HEAD'])
2907 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002908 (parsed_issue_arg.issue, patchset))
2909 print('Note: this created a local commit which does not have '
2910 'the same hash as the one uploaded for review. This will make '
2911 'uploading changes based on top of this branch difficult.\n'
2912 'If you want to do that, use "git cl patch --force" instead.')
2913
Stefan Zagerd08043c2017-10-12 12:07:02 -07002914 if self.GetBranch():
2915 self.SetIssue(parsed_issue_arg.issue)
2916 self.SetPatchset(patchset)
2917 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2918 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2919 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2920 else:
2921 print('WARNING: You are in detached HEAD state.\n'
2922 'The patch has been applied to your checkout, but you will not be '
2923 'able to upload a new patch set to the gerrit issue.\n'
2924 'Try using the \'-b\' option if you would like to work on a '
2925 'branch and/or upload a new patch set.')
2926
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002927 return 0
2928
2929 @staticmethod
2930 def ParseIssueURL(parsed_url):
2931 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2932 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002933 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2934 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002935 # Short urls like https://domain/<issue_number> can be used, but don't allow
2936 # specifying the patchset (you'd 404), but we allow that here.
2937 if parsed_url.path == '/':
2938 part = parsed_url.fragment
2939 else:
2940 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002941 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002942 if match:
2943 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002944 issue=int(match.group(3)),
2945 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002946 hostname=parsed_url.netloc,
2947 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002948 return None
2949
tandrii16e0b4e2016-06-07 10:34:28 -07002950 def _GerritCommitMsgHookCheck(self, offer_removal):
2951 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2952 if not os.path.exists(hook):
2953 return
2954 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2955 # custom developer made one.
2956 data = gclient_utils.FileRead(hook)
2957 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2958 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002959 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002960 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002961 'and may interfere with it in subtle ways.\n'
2962 'We recommend you remove the commit-msg hook.')
2963 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002964 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002965 gclient_utils.rm_file_or_tree(hook)
2966 print('Gerrit commit-msg hook removed.')
2967 else:
2968 print('OK, will keep Gerrit commit-msg hook in place.')
2969
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002970 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002971 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002972 if options.squash and options.no_squash:
2973 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002974
2975 if not options.squash and not options.no_squash:
2976 # Load default for user, repo, squash=true, in this order.
2977 options.squash = settings.GetSquashGerritUploads()
2978 elif options.no_squash:
2979 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002980
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002981 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002982 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002983
Aaron Gableb56ad332017-01-06 15:24:31 -08002984 # This may be None; default fallback value is determined in logic below.
2985 title = options.title
2986
Dominic Battre7d1c4842017-10-27 09:17:28 +02002987 # Extract bug number from branch name.
2988 bug = options.bug
2989 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2990 if not bug and match:
2991 bug = match.group(1)
2992
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002993 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002994 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002995 if self.GetIssue():
2996 # Try to get the message from a previous upload.
2997 message = self.GetDescription()
2998 if not message:
2999 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08003000 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003001 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08003002 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003003 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07003004 # When uploading a subsequent patchset, -m|--message is taken
3005 # as the patchset title if --title was not provided.
3006 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003007 else:
3008 default_title = RunGit(
3009 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07003010 if options.force:
3011 title = default_title
3012 else:
3013 title = ask_for_data(
3014 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015 change_id = self._GetChangeDetail()['change_id']
3016 while True:
3017 footer_change_ids = git_footers.get_footer_change_id(message)
3018 if footer_change_ids == [change_id]:
3019 break
3020 if not footer_change_ids:
3021 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003022 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003023 continue
3024 # There is already a valid footer but with different or several ids.
3025 # Doing this automatically is non-trivial as we don't want to lose
3026 # existing other footers, yet we want to append just 1 desired
3027 # Change-Id. Thus, just create a new footer, but let user verify the
3028 # new description.
3029 message = '%s\n\nChange-Id: %s' % (message, change_id)
3030 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08003031 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003032 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08003033 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003034 'Please, check the proposed correction to the description, '
3035 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
3036 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
3037 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003038 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003039 if not options.force:
3040 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02003041 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003042 message = change_desc.description
3043 if not message:
3044 DieWithError("Description is empty. Aborting...")
3045 # Continue the while loop.
3046 # Sanity check of this code - we should end up with proper message
3047 # footer.
3048 assert [change_id] == git_footers.get_footer_change_id(message)
3049 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08003050 else: # if not self.GetIssue()
3051 if options.message:
3052 message = options.message
3053 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003054 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08003055 if options.title:
3056 message = options.title + '\n\n' + message
3057 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003058
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003059 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02003060 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08003061 # On first upload, patchset title is always this string, while
3062 # --title flag gets converted to first line of message.
3063 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003064 if not change_desc.description:
3065 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003066 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003067 if len(change_ids) > 1:
3068 DieWithError('too many Change-Id footers, at most 1 allowed.')
3069 if not change_ids:
3070 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02003071 change_desc.set_description(git_footers.add_footer_change_id(
3072 change_desc.description,
3073 GenerateGerritChangeId(change_desc.description)))
3074 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003075 assert len(change_ids) == 1
3076 change_id = change_ids[0]
3077
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)
3081
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003082 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003083 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3084 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003085 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003086 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3087 desc_tempfile.write(change_desc.description)
3088 desc_tempfile.close()
3089 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3090 '-F', desc_tempfile.name]).strip()
3091 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003092 else:
3093 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003094 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003095 if not change_desc.description:
3096 DieWithError("Description is empty. Aborting...")
3097
3098 if not git_footers.get_footer_change_id(change_desc.description):
3099 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003100 change_desc.set_description(
3101 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003102 if options.reviewers or options.tbrs or options.add_owners_to:
3103 change_desc.update_reviewers(options.reviewers, options.tbrs,
3104 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003105 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003106 # For no-squash mode, we assume the remote called "origin" is the one we
3107 # want. It is not worthwhile to support different workflows for
3108 # no-squash mode.
3109 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003110 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3111
3112 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00003113 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003114 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3115 ref_to_push)]).splitlines()
3116 if len(commits) > 1:
3117 print('WARNING: This will upload %d commits. Run the following command '
3118 'to see which commits will be uploaded: ' % len(commits))
3119 print('git log %s..%s' % (parent, ref_to_push))
3120 print('You can also use `git squash-branch` to squash these into a '
3121 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003122 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003123
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003124 if options.reviewers or options.tbrs or options.add_owners_to:
3125 change_desc.update_reviewers(options.reviewers, options.tbrs,
3126 options.add_owners_to, change)
3127
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003128 # Extra options that can be specified at push time. Doc:
3129 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003130 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003131
Aaron Gable844cf292017-06-28 11:32:59 -07003132 # By default, new changes are started in WIP mode, and subsequent patchsets
3133 # don't send email. At any time, passing --send-mail will mark the change
3134 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003135 if options.send_mail:
3136 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003137 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04003138 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003139 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003140 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003141 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003142
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003143 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003144 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003145
Aaron Gable9b713dd2016-12-14 16:04:21 -08003146 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003147 # Punctuation and whitespace in |title| must be percent-encoded.
3148 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003149
agablec6787972016-09-09 16:13:34 -07003150 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003151 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003152
rmistry9eadede2016-09-19 11:22:43 -07003153 if options.topic:
3154 # Documentation on Gerrit topics is here:
3155 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003156 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003157
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003158 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003159 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003160 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003161 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003162 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3163
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003164 refspec_suffix = ''
3165 if refspec_opts:
3166 refspec_suffix = '%' + ','.join(refspec_opts)
3167 assert ' ' not in refspec_suffix, (
3168 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3169 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3170
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003171 try:
Edward Lemur83bd7f42018-10-10 00:14:21 +00003172 # TODO(crbug.com/881860): Remove.
3173 # Get interesting headers from git push, to be displayed to the user if
3174 # subsequent Gerrit RPC calls fail.
3175 env = os.environ.copy()
3176 env['GIT_CURL_VERBOSE'] = '1'
3177 class FilterHeaders(object):
3178 """Filter git push headers and store them in a file.
3179
3180 Regular git push output is printed directly.
3181 """
3182
3183 def __init__(self):
3184 # The output from git push that we want to store in a file.
3185 self._output = ''
3186 # Keeps track of whether the current line is part of a request header.
3187 self._on_header = False
3188 # Keeps track of repeated empty lines, which mark the end of a request
3189 # header.
3190 self._last_line_empty = False
3191
3192 def __call__(self, line):
3193 """Handle a single line of git push output."""
3194 if not line:
3195 # Two consecutive empty lines mark the end of a header.
3196 if self._last_line_empty:
3197 self._on_header = False
3198 self._last_line_empty = True
3199 return
3200
3201 self._last_line_empty = False
3202 # A line starting with '>' marks the beggining of a request header.
3203 if line[0] == '>':
3204 self._on_header = True
3205 GERRIT_ERR_LOGGER.info(line)
3206 # Lines not starting with '*' or '<', and not part of a request header
3207 # should be displayed to the user.
3208 elif line[0] not in '*<' and not self._on_header:
3209 print(line)
3210 # Flush after every line: useful for seeing progress when running as
3211 # recipe.
3212 sys.stdout.flush()
3213 # Filter out the cookie and authorization headers.
3214 elif ('cookie: ' not in line.lower()
3215 and 'authorization: ' not in line.lower()):
3216 GERRIT_ERR_LOGGER.info(line)
3217
3218 filter_fn = FilterHeaders()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003219 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003220 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00003221 print_stdout=False,
3222 filter_fn=filter_fn,
3223 env=env)
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003224 except subprocess2.CalledProcessError:
3225 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003226 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003227 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003228 'credential problems:\n'
3229 ' git cl creds-check\n',
3230 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003231
3232 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003233 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003234 change_numbers = [m.group(1)
3235 for m in map(regex.match, push_stdout.splitlines())
3236 if m]
3237 if len(change_numbers) != 1:
3238 DieWithError(
3239 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003240 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003241 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003242 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003243
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003244 reviewers = sorted(change_desc.get_reviewers())
3245
tandrii88189772016-09-29 04:29:57 -07003246 # Add cc's from the CC_LIST and --cc flag (if any).
Sergiy Byelozyorovaaf2cc02018-09-24 18:02:28 +00003247 if not options.private and not options.no_autocc:
Aaron Gabled1052492017-05-15 15:05:34 -07003248 cc = self.GetCCList().split(',')
3249 else:
3250 cc = []
tandrii88189772016-09-29 04:29:57 -07003251 if options.cc:
3252 cc.extend(options.cc)
3253 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003254 if change_desc.get_cced():
3255 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003256
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003257 if self.GetIssue():
3258 # GetIssue() is not set in case of non-squash uploads according to tests.
3259 # TODO(agable): non-squash uploads in git cl should be removed.
3260 gerrit_util.AddReviewers(
3261 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003262 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003263 reviewers, cc,
3264 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003265
Aaron Gablefd238082017-06-07 13:42:34 -07003266 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003267 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3268 score = 1
3269 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3270 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3271 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003272 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003273 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003274 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003275 msg='Self-approving for TBR',
3276 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003277
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003278 return 0
3279
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003280 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3281 change_desc):
3282 """Computes parent of the generated commit to be uploaded to Gerrit.
3283
3284 Returns revision or a ref name.
3285 """
3286 if custom_cl_base:
3287 # Try to avoid creating additional unintended CLs when uploading, unless
3288 # user wants to take this risk.
3289 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3290 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3291 local_ref_of_target_remote])
3292 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003293 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003294 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3295 'If you proceed with upload, more than 1 CL may be created by '
3296 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3297 'If you are certain that specified base `%s` has already been '
3298 'uploaded to Gerrit as another CL, you may proceed.\n' %
3299 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3300 if not force:
3301 confirm_or_exit(
3302 'Do you take responsibility for cleaning up potential mess '
3303 'resulting from proceeding with upload?',
3304 action='upload')
3305 return custom_cl_base
3306
Aaron Gablef97e33d2017-03-30 15:44:27 -07003307 if remote != '.':
3308 return self.GetCommonAncestorWithUpstream()
3309
3310 # If our upstream branch is local, we base our squashed commit on its
3311 # squashed version.
3312 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3313
Aaron Gablef97e33d2017-03-30 15:44:27 -07003314 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003315 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003316
3317 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003318 # TODO(tandrii): consider checking parent change in Gerrit and using its
3319 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3320 # the tree hash of the parent branch. The upside is less likely bogus
3321 # requests to reupload parent change just because it's uploadhash is
3322 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003323 parent = RunGit(['config',
3324 'branch.%s.gerritsquashhash' % upstream_branch_name],
3325 error_ok=True).strip()
3326 # Verify that the upstream branch has been uploaded too, otherwise
3327 # Gerrit will create additional CLs when uploading.
3328 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3329 RunGitSilent(['rev-parse', parent + ':'])):
3330 DieWithError(
3331 '\nUpload upstream branch %s first.\n'
3332 'It is likely that this branch has been rebased since its last '
3333 'upload, so you just need to upload it again.\n'
3334 '(If you uploaded it with --no-squash, then branch dependencies '
3335 'are not supported, and you should reupload with --squash.)'
3336 % upstream_branch_name,
3337 change_desc)
3338 return parent
3339
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003340 def _AddChangeIdToCommitMessage(self, options, args):
3341 """Re-commits using the current message, assumes the commit hook is in
3342 place.
3343 """
3344 log_desc = options.message or CreateDescriptionFromLog(args)
3345 git_command = ['commit', '--amend', '-m', log_desc]
3346 RunGit(git_command)
3347 new_log_desc = CreateDescriptionFromLog(args)
3348 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003349 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003350 return new_log_desc
3351 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003352 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003353
Ravi Mistry31e7d562018-04-02 12:53:57 -04003354 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3355 """Sets labels on the change based on the provided flags."""
3356 labels = {}
3357 notify = None;
3358 if enable_auto_submit:
3359 labels['Auto-Submit'] = 1
3360 if use_commit_queue:
3361 labels['Commit-Queue'] = 2
3362 elif cq_dry_run:
3363 labels['Commit-Queue'] = 1
3364 notify = False
3365 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003366 gerrit_util.SetReview(
3367 self._GetGerritHost(),
3368 self._GerritChangeIdentifier(),
3369 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003370
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003371 def SetCQState(self, new_state):
3372 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003373 vote_map = {
3374 _CQState.NONE: 0,
3375 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003376 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003377 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003378 labels = {'Commit-Queue': vote_map[new_state]}
3379 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003380 gerrit_util.SetReview(
3381 self._GetGerritHost(), self._GerritChangeIdentifier(),
3382 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003383
tandriie113dfd2016-10-11 10:20:12 -07003384 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003385 try:
3386 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003387 except GerritChangeNotExists:
3388 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003389
3390 if data['status'] in ('ABANDONED', 'MERGED'):
3391 return 'CL %s is closed' % self.GetIssue()
3392
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003393 def GetTryJobProperties(self, patchset=None):
3394 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003395 data = self._GetChangeDetail(['ALL_REVISIONS'])
3396 patchset = int(patchset or self.GetPatchset())
3397 assert patchset
3398 revision_data = None # Pylint wants it to be defined.
3399 for revision_data in data['revisions'].itervalues():
3400 if int(revision_data['_number']) == patchset:
3401 break
3402 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003403 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003404 (patchset, self.GetIssue()))
3405 return {
3406 'patch_issue': self.GetIssue(),
3407 'patch_set': patchset or self.GetPatchset(),
3408 'patch_project': data['project'],
3409 'patch_storage': 'gerrit',
3410 'patch_ref': revision_data['fetch']['http']['ref'],
3411 'patch_repository_url': revision_data['fetch']['http']['url'],
3412 'patch_gerrit_url': self.GetCodereviewServer(),
3413 }
tandriie113dfd2016-10-11 10:20:12 -07003414
tandriide281ae2016-10-12 06:02:30 -07003415 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003416 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003417
Edward Lemur707d70b2018-02-07 00:50:14 +01003418 def GetReviewers(self):
3419 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3420 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3421
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003422
3423_CODEREVIEW_IMPLEMENTATIONS = {
3424 'rietveld': _RietveldChangelistImpl,
3425 'gerrit': _GerritChangelistImpl,
3426}
3427
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003428
iannuccie53c9352016-08-17 14:40:40 -07003429def _add_codereview_issue_select_options(parser, extra=""):
3430 _add_codereview_select_options(parser)
3431
3432 text = ('Operate on this issue number instead of the current branch\'s '
3433 'implicit issue.')
3434 if extra:
3435 text += ' '+extra
3436 parser.add_option('-i', '--issue', type=int, help=text)
3437
3438
3439def _process_codereview_issue_select_options(parser, options):
3440 _process_codereview_select_options(parser, options)
3441 if options.issue is not None and not options.forced_codereview:
3442 parser.error('--issue must be specified with either --rietveld or --gerrit')
3443
3444
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003445def _add_codereview_select_options(parser):
3446 """Appends --gerrit and --rietveld options to force specific codereview."""
3447 parser.codereview_group = optparse.OptionGroup(
3448 parser, 'EXPERIMENTAL! Codereview override options')
3449 parser.add_option_group(parser.codereview_group)
3450 parser.codereview_group.add_option(
3451 '--gerrit', action='store_true',
3452 help='Force the use of Gerrit for codereview')
3453 parser.codereview_group.add_option(
3454 '--rietveld', action='store_true',
3455 help='Force the use of Rietveld for codereview')
3456
3457
3458def _process_codereview_select_options(parser, options):
3459 if options.gerrit and options.rietveld:
3460 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3461 options.forced_codereview = None
3462 if options.gerrit:
3463 options.forced_codereview = 'gerrit'
3464 elif options.rietveld:
3465 options.forced_codereview = 'rietveld'
3466
3467
tandriif9aefb72016-07-01 09:06:51 -07003468def _get_bug_line_values(default_project, bugs):
3469 """Given default_project and comma separated list of bugs, yields bug line
3470 values.
3471
3472 Each bug can be either:
3473 * a number, which is combined with default_project
3474 * string, which is left as is.
3475
3476 This function may produce more than one line, because bugdroid expects one
3477 project per line.
3478
3479 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3480 ['v8:123', 'chromium:789']
3481 """
3482 default_bugs = []
3483 others = []
3484 for bug in bugs.split(','):
3485 bug = bug.strip()
3486 if bug:
3487 try:
3488 default_bugs.append(int(bug))
3489 except ValueError:
3490 others.append(bug)
3491
3492 if default_bugs:
3493 default_bugs = ','.join(map(str, default_bugs))
3494 if default_project:
3495 yield '%s:%s' % (default_project, default_bugs)
3496 else:
3497 yield default_bugs
3498 for other in sorted(others):
3499 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3500 yield other
3501
3502
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003503class ChangeDescription(object):
3504 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003505 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003506 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003507 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003508 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003509 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3510 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3511 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3512 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003513
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003514 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003515 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003516
agable@chromium.org42c20792013-09-12 17:34:49 +00003517 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003518 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003519 return '\n'.join(self._description_lines)
3520
3521 def set_description(self, desc):
3522 if isinstance(desc, basestring):
3523 lines = desc.splitlines()
3524 else:
3525 lines = [line.rstrip() for line in desc]
3526 while lines and not lines[0]:
3527 lines.pop(0)
3528 while lines and not lines[-1]:
3529 lines.pop(-1)
3530 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003531
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003532 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3533 """Rewrites the R=/TBR= line(s) as a single line each.
3534
3535 Args:
3536 reviewers (list(str)) - list of additional emails to use for reviewers.
3537 tbrs (list(str)) - list of additional emails to use for TBRs.
3538 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3539 the change that are missing OWNER coverage. If this is not None, you
3540 must also pass a value for `change`.
3541 change (Change) - The Change that should be used for OWNERS lookups.
3542 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003543 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003544 assert isinstance(tbrs, list), tbrs
3545
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003546 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003547 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003548
3549 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003550 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003551
3552 reviewers = set(reviewers)
3553 tbrs = set(tbrs)
3554 LOOKUP = {
3555 'TBR': tbrs,
3556 'R': reviewers,
3557 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003558
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003559 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003560 regexp = re.compile(self.R_LINE)
3561 matches = [regexp.match(line) for line in self._description_lines]
3562 new_desc = [l for i, l in enumerate(self._description_lines)
3563 if not matches[i]]
3564 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003565
agable@chromium.org42c20792013-09-12 17:34:49 +00003566 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003567
3568 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003569 for match in matches:
3570 if not match:
3571 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003572 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3573
3574 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003575 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003576 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003577 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003578 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003579 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003580 LOOKUP[add_owners_to].update(
3581 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003582
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003583 # If any folks ended up in both groups, remove them from tbrs.
3584 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003585
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003586 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3587 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003588
3589 # Put the new lines in the description where the old first R= line was.
3590 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3591 if 0 <= line_loc < len(self._description_lines):
3592 if new_tbr_line:
3593 self._description_lines.insert(line_loc, new_tbr_line)
3594 if new_r_line:
3595 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003596 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003597 if new_r_line:
3598 self.append_footer(new_r_line)
3599 if new_tbr_line:
3600 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003601
Aaron Gable3a16ed12017-03-23 10:51:55 -07003602 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003603 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003604 self.set_description([
3605 '# Enter a description of the change.',
3606 '# This will be displayed on the codereview site.',
3607 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003608 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003609 '--------------------',
3610 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003611
agable@chromium.org42c20792013-09-12 17:34:49 +00003612 regexp = re.compile(self.BUG_LINE)
3613 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003614 prefix = settings.GetBugPrefix()
3615 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003616 if git_footer:
3617 self.append_footer('Bug: %s' % ', '.join(values))
3618 else:
3619 for value in values:
3620 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003621
agable@chromium.org42c20792013-09-12 17:34:49 +00003622 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003623 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003624 if not content:
3625 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003626 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003627
Bruce Dawson2377b012018-01-11 16:46:49 -08003628 # Strip off comments and default inserted "Bug:" line.
3629 clean_lines = [line.rstrip() for line in lines if not
3630 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003631 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003632 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003633 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003634
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003635 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003636 """Adds a footer line to the description.
3637
3638 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3639 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3640 that Gerrit footers are always at the end.
3641 """
3642 parsed_footer_line = git_footers.parse_footer(line)
3643 if parsed_footer_line:
3644 # Line is a gerrit footer in the form: Footer-Key: any value.
3645 # Thus, must be appended observing Gerrit footer rules.
3646 self.set_description(
3647 git_footers.add_footer(self.description,
3648 key=parsed_footer_line[0],
3649 value=parsed_footer_line[1]))
3650 return
3651
3652 if not self._description_lines:
3653 self._description_lines.append(line)
3654 return
3655
3656 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3657 if gerrit_footers:
3658 # git_footers.split_footers ensures that there is an empty line before
3659 # actual (gerrit) footers, if any. We have to keep it that way.
3660 assert top_lines and top_lines[-1] == ''
3661 top_lines, separator = top_lines[:-1], top_lines[-1:]
3662 else:
3663 separator = [] # No need for separator if there are no gerrit_footers.
3664
3665 prev_line = top_lines[-1] if top_lines else ''
3666 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3667 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3668 top_lines.append('')
3669 top_lines.append(line)
3670 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003671
tandrii99a72f22016-08-17 14:33:24 -07003672 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003673 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003674 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003675 reviewers = [match.group(2).strip()
3676 for match in matches
3677 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003678 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003679
bradnelsond975b302016-10-23 12:20:23 -07003680 def get_cced(self):
3681 """Retrieves the list of reviewers."""
3682 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3683 cced = [match.group(2).strip() for match in matches if match]
3684 return cleanup_list(cced)
3685
Nodir Turakulov23b82142017-11-16 11:04:25 -08003686 def get_hash_tags(self):
3687 """Extracts and sanitizes a list of Gerrit hashtags."""
3688 subject = (self._description_lines or ('',))[0]
3689 subject = re.sub(
3690 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3691
3692 tags = []
3693 start = 0
3694 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3695 while True:
3696 m = bracket_exp.match(subject, start)
3697 if not m:
3698 break
3699 tags.append(self.sanitize_hash_tag(m.group(1)))
3700 start = m.end()
3701
3702 if not tags:
3703 # Try "Tag: " prefix.
3704 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3705 if m:
3706 tags.append(self.sanitize_hash_tag(m.group(1)))
3707 return tags
3708
3709 @classmethod
3710 def sanitize_hash_tag(cls, tag):
3711 """Returns a sanitized Gerrit hash tag.
3712
3713 A sanitized hashtag can be used as a git push refspec parameter value.
3714 """
3715 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3716
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003717 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3718 """Updates this commit description given the parent.
3719
3720 This is essentially what Gnumbd used to do.
3721 Consult https://goo.gl/WMmpDe for more details.
3722 """
3723 assert parent_msg # No, orphan branch creation isn't supported.
3724 assert parent_hash
3725 assert dest_ref
3726 parent_footer_map = git_footers.parse_footers(parent_msg)
3727 # This will also happily parse svn-position, which GnumbD is no longer
3728 # supporting. While we'd generate correct footers, the verifier plugin
3729 # installed in Gerrit will block such commit (ie git push below will fail).
3730 parent_position = git_footers.get_position(parent_footer_map)
3731
3732 # Cherry-picks may have last line obscuring their prior footers,
3733 # from git_footers perspective. This is also what Gnumbd did.
3734 cp_line = None
3735 if (self._description_lines and
3736 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3737 cp_line = self._description_lines.pop()
3738
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003739 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003740
3741 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3742 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003743 for i, line in enumerate(footer_lines):
3744 k, v = git_footers.parse_footer(line) or (None, None)
3745 if k and k.startswith('Cr-'):
3746 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003747
3748 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003749 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003750 if parent_position[0] == dest_ref:
3751 # Same branch as parent.
3752 number = int(parent_position[1]) + 1
3753 else:
3754 number = 1 # New branch, and extra lineage.
3755 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3756 int(parent_position[1])))
3757
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003758 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3759 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003760
3761 self._description_lines = top_lines
3762 if cp_line:
3763 self._description_lines.append(cp_line)
3764 if self._description_lines[-1] != '':
3765 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003766 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003767
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003768
Aaron Gablea1bab272017-04-11 16:38:18 -07003769def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003770 """Retrieves the reviewers that approved a CL from the issue properties with
3771 messages.
3772
3773 Note that the list may contain reviewers that are not committer, thus are not
3774 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003775
3776 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003777 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003778 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003779 return sorted(
3780 set(
3781 message['sender']
3782 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003783 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003784 )
3785 )
3786
3787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003788def FindCodereviewSettingsFile(filename='codereview.settings'):
3789 """Finds the given file starting in the cwd and going up.
3790
3791 Only looks up to the top of the repository unless an
3792 'inherit-review-settings-ok' file exists in the root of the repository.
3793 """
3794 inherit_ok_file = 'inherit-review-settings-ok'
3795 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003796 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003797 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3798 root = '/'
3799 while True:
3800 if filename in os.listdir(cwd):
3801 if os.path.isfile(os.path.join(cwd, filename)):
3802 return open(os.path.join(cwd, filename))
3803 if cwd == root:
3804 break
3805 cwd = os.path.dirname(cwd)
3806
3807
3808def LoadCodereviewSettingsFromFile(fileobj):
3809 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003810 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003812 def SetProperty(name, setting, unset_error_ok=False):
3813 fullname = 'rietveld.' + name
3814 if setting in keyvals:
3815 RunGit(['config', fullname, keyvals[setting]])
3816 else:
3817 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3818
tandrii48df5812016-10-17 03:55:37 -07003819 if not keyvals.get('GERRIT_HOST', False):
3820 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003821 # Only server setting is required. Other settings can be absent.
3822 # In that case, we ignore errors raised during option deletion attempt.
3823 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003824 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3826 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003827 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003828 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3829 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003830 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003831 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3832 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003834 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003835 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003836
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003837 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003838 RunGit(['config', 'gerrit.squash-uploads',
3839 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003840
tandrii@chromium.org28253532016-04-14 13:46:56 +00003841 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003842 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003843 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003846 # should be of the form
3847 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3848 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003849 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3850 keyvals['ORIGIN_URL_CONFIG']])
3851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003852
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003853def urlretrieve(source, destination):
3854 """urllib is broken for SSL connections via a proxy therefore we
3855 can't use urllib.urlretrieve()."""
3856 with open(destination, 'w') as f:
3857 f.write(urllib2.urlopen(source).read())
3858
3859
ukai@chromium.org712d6102013-11-27 00:52:58 +00003860def hasSheBang(fname):
3861 """Checks fname is a #! script."""
3862 with open(fname) as f:
3863 return f.read(2).startswith('#!')
3864
3865
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003866# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3867def DownloadHooks(*args, **kwargs):
3868 pass
3869
3870
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003871def DownloadGerritHook(force):
3872 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003873
3874 Args:
3875 force: True to update hooks. False to install hooks if not present.
3876 """
3877 if not settings.GetIsGerrit():
3878 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003879 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003880 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3881 if not os.access(dst, os.X_OK):
3882 if os.path.exists(dst):
3883 if not force:
3884 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003885 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003886 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003887 if not hasSheBang(dst):
3888 DieWithError('Not a script: %s\n'
3889 'You need to download from\n%s\n'
3890 'into .git/hooks/commit-msg and '
3891 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003892 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3893 except Exception:
3894 if os.path.exists(dst):
3895 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003896 DieWithError('\nFailed to download hooks.\n'
3897 'You need to download from\n%s\n'
3898 'into .git/hooks/commit-msg and '
3899 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003900
3901
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003902def GetRietveldCodereviewSettingsInteractively():
3903 """Prompt the user for settings."""
3904 server = settings.GetDefaultServerUrl(error_ok=True)
3905 prompt = 'Rietveld server (host[:port])'
3906 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3907 newserver = ask_for_data(prompt + ':')
3908 if not server and not newserver:
3909 newserver = DEFAULT_SERVER
3910 if newserver:
3911 newserver = gclient_utils.UpgradeToHttps(newserver)
3912 if newserver != server:
3913 RunGit(['config', 'rietveld.server', newserver])
3914
3915 def SetProperty(initial, caption, name, is_url):
3916 prompt = caption
3917 if initial:
3918 prompt += ' ("x" to clear) [%s]' % initial
3919 new_val = ask_for_data(prompt + ':')
3920 if new_val == 'x':
3921 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3922 elif new_val:
3923 if is_url:
3924 new_val = gclient_utils.UpgradeToHttps(new_val)
3925 if new_val != initial:
3926 RunGit(['config', 'rietveld.' + name, new_val])
3927
3928 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3929 SetProperty(settings.GetDefaultPrivateFlag(),
3930 'Private flag (rietveld only)', 'private', False)
3931 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3932 'tree-status-url', False)
3933 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3934 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3935 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3936 'run-post-upload-hook', False)
3937
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003938
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003939class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003940 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003941
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003942 _GOOGLESOURCE = 'googlesource.com'
3943
3944 def __init__(self):
3945 # Cached list of [host, identity, source], where source is either
3946 # .gitcookies or .netrc.
3947 self._all_hosts = None
3948
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003949 def ensure_configured_gitcookies(self):
3950 """Runs checks and suggests fixes to make git use .gitcookies from default
3951 path."""
3952 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3953 configured_path = RunGitSilent(
3954 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003955 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003956 if configured_path:
3957 self._ensure_default_gitcookies_path(configured_path, default)
3958 else:
3959 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003960
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003961 @staticmethod
3962 def _ensure_default_gitcookies_path(configured_path, default_path):
3963 assert configured_path
3964 if configured_path == default_path:
3965 print('git is already configured to use your .gitcookies from %s' %
3966 configured_path)
3967 return
3968
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003969 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003970 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3971 (configured_path, default_path))
3972
3973 if not os.path.exists(configured_path):
3974 print('However, your configured .gitcookies file is missing.')
3975 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3976 action='reconfigure')
3977 RunGit(['config', '--global', 'http.cookiefile', default_path])
3978 return
3979
3980 if os.path.exists(default_path):
3981 print('WARNING: default .gitcookies file already exists %s' %
3982 default_path)
3983 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3984 default_path)
3985
3986 confirm_or_exit('Move existing .gitcookies to default location?',
3987 action='move')
3988 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003989 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003990 print('Moved and reconfigured git to use .gitcookies from %s' %
3991 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003992
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003993 @staticmethod
3994 def _configure_gitcookies_path(default_path):
3995 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3996 if os.path.exists(netrc_path):
3997 print('You seem to be using outdated .netrc for git credentials: %s' %
3998 netrc_path)
3999 print('This tool will guide you through setting up recommended '
4000 '.gitcookies store for git credentials.\n'
4001 '\n'
4002 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
4003 ' git config --global --unset http.cookiefile\n'
4004 ' mv %s %s.backup\n\n' % (default_path, default_path))
4005 confirm_or_exit(action='setup .gitcookies')
4006 RunGit(['config', '--global', 'http.cookiefile', default_path])
4007 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004008
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004009 def get_hosts_with_creds(self, include_netrc=False):
4010 if self._all_hosts is None:
4011 a = gerrit_util.CookiesAuthenticator()
4012 self._all_hosts = [
4013 (h, u, s)
4014 for h, u, s in itertools.chain(
4015 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
4016 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
4017 )
4018 if h.endswith(self._GOOGLESOURCE)
4019 ]
4020
4021 if include_netrc:
4022 return self._all_hosts
4023 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
4024
4025 def print_current_creds(self, include_netrc=False):
4026 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
4027 if not hosts:
4028 print('No Git/Gerrit credentials found')
4029 return
4030 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
4031 header = [('Host', 'User', 'Which file'),
4032 ['=' * l for l in lengths]]
4033 for row in (header + hosts):
4034 print('\t'.join((('%%+%ds' % l) % s)
4035 for l, s in zip(lengths, row)))
4036
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004037 @staticmethod
4038 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08004039 """Parses identity "git-<username>.domain" into <username> and domain."""
4040 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02004041 # distinguishable from sub-domains. But we do know typical domains:
4042 if identity.endswith('.chromium.org'):
4043 domain = 'chromium.org'
4044 username = identity[:-len('.chromium.org')]
4045 else:
4046 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004047 if username.startswith('git-'):
4048 username = username[len('git-'):]
4049 return username, domain
4050
4051 def _get_usernames_of_domain(self, domain):
4052 """Returns list of usernames referenced by .gitcookies in a given domain."""
4053 identities_by_domain = {}
4054 for _, identity, _ in self.get_hosts_with_creds():
4055 username, domain = self._parse_identity(identity)
4056 identities_by_domain.setdefault(domain, []).append(username)
4057 return identities_by_domain.get(domain)
4058
4059 def _canonical_git_googlesource_host(self, host):
4060 """Normalizes Gerrit hosts (with '-review') to Git host."""
4061 assert host.endswith(self._GOOGLESOURCE)
4062 # Prefix doesn't include '.' at the end.
4063 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
4064 if prefix.endswith('-review'):
4065 prefix = prefix[:-len('-review')]
4066 return prefix + '.' + self._GOOGLESOURCE
4067
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004068 def _canonical_gerrit_googlesource_host(self, host):
4069 git_host = self._canonical_git_googlesource_host(host)
4070 prefix = git_host.split('.', 1)[0]
4071 return prefix + '-review.' + self._GOOGLESOURCE
4072
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004073 def _get_counterpart_host(self, host):
4074 assert host.endswith(self._GOOGLESOURCE)
4075 git = self._canonical_git_googlesource_host(host)
4076 gerrit = self._canonical_gerrit_googlesource_host(git)
4077 return git if gerrit == host else gerrit
4078
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004079 def has_generic_host(self):
4080 """Returns whether generic .googlesource.com has been configured.
4081
4082 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
4083 """
4084 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
4085 if host == '.' + self._GOOGLESOURCE:
4086 return True
4087 return False
4088
4089 def _get_git_gerrit_identity_pairs(self):
4090 """Returns map from canonic host to pair of identities (Git, Gerrit).
4091
4092 One of identities might be None, meaning not configured.
4093 """
4094 host_to_identity_pairs = {}
4095 for host, identity, _ in self.get_hosts_with_creds():
4096 canonical = self._canonical_git_googlesource_host(host)
4097 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
4098 idx = 0 if canonical == host else 1
4099 pair[idx] = identity
4100 return host_to_identity_pairs
4101
4102 def get_partially_configured_hosts(self):
4103 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004104 (host if i1 else self._canonical_gerrit_googlesource_host(host))
4105 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
4106 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004107
4108 def get_conflicting_hosts(self):
4109 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004110 host
4111 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004112 if None not in (i1, i2) and i1 != i2)
4113
4114 def get_duplicated_hosts(self):
4115 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
4116 return set(host for host, count in counters.iteritems() if count > 1)
4117
4118 _EXPECTED_HOST_IDENTITY_DOMAINS = {
4119 'chromium.googlesource.com': 'chromium.org',
4120 'chrome-internal.googlesource.com': 'google.com',
4121 }
4122
4123 def get_hosts_with_wrong_identities(self):
4124 """Finds hosts which **likely** reference wrong identities.
4125
4126 Note: skips hosts which have conflicting identities for Git and Gerrit.
4127 """
4128 hosts = set()
4129 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4130 pair = self._get_git_gerrit_identity_pairs().get(host)
4131 if pair and pair[0] == pair[1]:
4132 _, domain = self._parse_identity(pair[0])
4133 if domain != expected:
4134 hosts.add(host)
4135 return hosts
4136
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004137 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004138 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004139 hosts = sorted(hosts)
4140 assert hosts
4141 if extra_column_func is None:
4142 extras = [''] * len(hosts)
4143 else:
4144 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004145 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4146 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004147 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004148 lines.append(tmpl % he)
4149 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004150
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004151 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004152 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004153 yield ('.googlesource.com wildcard record detected',
4154 ['Chrome Infrastructure team recommends to list full host names '
4155 'explicitly.'],
4156 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004157
4158 dups = self.get_duplicated_hosts()
4159 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004160 yield ('The following hosts were defined twice',
4161 self._format_hosts(dups),
4162 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004163
4164 partial = self.get_partially_configured_hosts()
4165 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004166 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4167 'These hosts are missing',
4168 self._format_hosts(partial, lambda host: 'but %s defined' %
4169 self._get_counterpart_host(host)),
4170 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004171
4172 conflicting = self.get_conflicting_hosts()
4173 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004174 yield ('The following Git hosts have differing credentials from their '
4175 'Gerrit counterparts',
4176 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4177 tuple(self._get_git_gerrit_identity_pairs()[host])),
4178 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004179
4180 wrong = self.get_hosts_with_wrong_identities()
4181 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004182 yield ('These hosts likely use wrong identity',
4183 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4184 (self._get_git_gerrit_identity_pairs()[host][0],
4185 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4186 wrong)
4187
4188 def find_and_report_problems(self):
4189 """Returns True if there was at least one problem, else False."""
4190 found = False
4191 bad_hosts = set()
4192 for title, sublines, hosts in self._find_problems():
4193 if not found:
4194 found = True
4195 print('\n\n.gitcookies problem report:\n')
4196 bad_hosts.update(hosts or [])
4197 print(' %s%s' % (title , (':' if sublines else '')))
4198 if sublines:
4199 print()
4200 print(' %s' % '\n '.join(sublines))
4201 print()
4202
4203 if bad_hosts:
4204 assert found
4205 print(' You can manually remove corresponding lines in your %s file and '
4206 'visit the following URLs with correct account to generate '
4207 'correct credential lines:\n' %
4208 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4209 print(' %s' % '\n '.join(sorted(set(
4210 gerrit_util.CookiesAuthenticator().get_new_password_url(
4211 self._canonical_git_googlesource_host(host))
4212 for host in bad_hosts
4213 ))))
4214 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004215
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004216
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004217@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004218def CMDcreds_check(parser, args):
4219 """Checks credentials and suggests changes."""
4220 _, _ = parser.parse_args(args)
4221
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004222 # Code below checks .gitcookies. Abort if using something else.
4223 authn = gerrit_util.Authenticator.get()
4224 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4225 if isinstance(authn, gerrit_util.GceAuthenticator):
4226 DieWithError(
4227 'This command is not designed for GCE, are you on a bot?\n'
4228 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4229 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004230 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004231 'This command is not designed for bot environment. It checks '
4232 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004233
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004234 checker = _GitCookiesChecker()
4235 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004236
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004237 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004238 checker.print_current_creds(include_netrc=True)
4239
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004240 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004241 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004242 return 0
4243 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004244
4245
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004246@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004247@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004248def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004249 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004251 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004252 # TODO(tandrii): remove this once we switch to Gerrit.
4253 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004254 parser.add_option('--activate-update', action='store_true',
4255 help='activate auto-updating [rietveld] section in '
4256 '.git/config')
4257 parser.add_option('--deactivate-update', action='store_true',
4258 help='deactivate auto-updating [rietveld] section in '
4259 '.git/config')
4260 options, args = parser.parse_args(args)
4261
4262 if options.deactivate_update:
4263 RunGit(['config', 'rietveld.autoupdate', 'false'])
4264 return
4265
4266 if options.activate_update:
4267 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4268 return
4269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004271 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004272 return 0
4273
4274 url = args[0]
4275 if not url.endswith('codereview.settings'):
4276 url = os.path.join(url, 'codereview.settings')
4277
4278 # Load code review settings and download hooks (if available).
4279 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4280 return 0
4281
4282
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004283@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004284def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004285 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004286 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4287 branch = ShortBranchName(branchref)
4288 _, args = parser.parse_args(args)
4289 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004290 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004291 return RunGit(['config', 'branch.%s.base-url' % branch],
4292 error_ok=False).strip()
4293 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004294 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004295 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4296 error_ok=False).strip()
4297
4298
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004299def color_for_status(status):
4300 """Maps a Changelist status to color, for CMDstatus and other tools."""
4301 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004302 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004303 'waiting': Fore.BLUE,
4304 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004305 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004306 'lgtm': Fore.GREEN,
4307 'commit': Fore.MAGENTA,
4308 'closed': Fore.CYAN,
4309 'error': Fore.WHITE,
4310 }.get(status, Fore.WHITE)
4311
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004312
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004313def get_cl_statuses(changes, fine_grained, max_processes=None):
4314 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004315
4316 If fine_grained is true, this will fetch CL statuses from the server.
4317 Otherwise, simply indicate if there's a matching url for the given branches.
4318
4319 If max_processes is specified, it is used as the maximum number of processes
4320 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4321 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004322
4323 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004324 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004325 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004326 upload.verbosity = 0
4327
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004328 if not changes:
4329 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004330
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004331 if not fine_grained:
4332 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004333 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004334 for cl in changes:
4335 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004336 return
4337
4338 # First, sort out authentication issues.
4339 logging.debug('ensuring credentials exist')
4340 for cl in changes:
4341 cl.EnsureAuthenticated(force=False, refresh=True)
4342
4343 def fetch(cl):
4344 try:
4345 return (cl, cl.GetStatus())
4346 except:
4347 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004348 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004349 raise
4350
4351 threads_count = len(changes)
4352 if max_processes:
4353 threads_count = max(1, min(threads_count, max_processes))
4354 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4355
4356 pool = ThreadPool(threads_count)
4357 fetched_cls = set()
4358 try:
4359 it = pool.imap_unordered(fetch, changes).__iter__()
4360 while True:
4361 try:
4362 cl, status = it.next(timeout=5)
4363 except multiprocessing.TimeoutError:
4364 break
4365 fetched_cls.add(cl)
4366 yield cl, status
4367 finally:
4368 pool.close()
4369
4370 # Add any branches that failed to fetch.
4371 for cl in set(changes) - fetched_cls:
4372 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004373
rmistry@google.com2dd99862015-06-22 12:22:18 +00004374
4375def upload_branch_deps(cl, args):
4376 """Uploads CLs of local branches that are dependents of the current branch.
4377
4378 If the local branch dependency tree looks like:
4379 test1 -> test2.1 -> test3.1
4380 -> test3.2
4381 -> test2.2 -> test3.3
4382
4383 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4384 run on the dependent branches in this order:
4385 test2.1, test3.1, test3.2, test2.2, test3.3
4386
4387 Note: This function does not rebase your local dependent branches. Use it when
4388 you make a change to the parent branch that will not conflict with its
4389 dependent branches, and you would like their dependencies updated in
4390 Rietveld.
4391 """
4392 if git_common.is_dirty_git_tree('upload-branch-deps'):
4393 return 1
4394
4395 root_branch = cl.GetBranch()
4396 if root_branch is None:
4397 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4398 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004399 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004400 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4401 'patchset dependencies without an uploaded CL.')
4402
4403 branches = RunGit(['for-each-ref',
4404 '--format=%(refname:short) %(upstream:short)',
4405 'refs/heads'])
4406 if not branches:
4407 print('No local branches found.')
4408 return 0
4409
4410 # Create a dictionary of all local branches to the branches that are dependent
4411 # on it.
4412 tracked_to_dependents = collections.defaultdict(list)
4413 for b in branches.splitlines():
4414 tokens = b.split()
4415 if len(tokens) == 2:
4416 branch_name, tracked = tokens
4417 tracked_to_dependents[tracked].append(branch_name)
4418
vapiera7fbd5a2016-06-16 09:17:49 -07004419 print()
4420 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004421 dependents = []
4422 def traverse_dependents_preorder(branch, padding=''):
4423 dependents_to_process = tracked_to_dependents.get(branch, [])
4424 padding += ' '
4425 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004426 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004427 dependents.append(dependent)
4428 traverse_dependents_preorder(dependent, padding)
4429 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004430 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004431
4432 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004433 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004434 return 0
4435
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004436 confirm_or_exit('This command will checkout all dependent branches and run '
4437 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004438
andybons@chromium.org962f9462016-02-03 20:00:42 +00004439 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004440 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004441 args.extend(['-t', 'Updated patchset dependency'])
4442
rmistry@google.com2dd99862015-06-22 12:22:18 +00004443 # Record all dependents that failed to upload.
4444 failures = {}
4445 # Go through all dependents, checkout the branch and upload.
4446 try:
4447 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print()
4449 print('--------------------------------------')
4450 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004451 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004452 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004453 try:
4454 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004455 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004456 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004457 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004458 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004460 finally:
4461 # Swap back to the original root branch.
4462 RunGit(['checkout', '-q', root_branch])
4463
vapiera7fbd5a2016-06-16 09:17:49 -07004464 print()
4465 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004466 for dependent_branch in dependents:
4467 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004468 print(' %s : %s' % (dependent_branch, upload_status))
4469 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004470
4471 return 0
4472
4473
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004474@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004475def CMDarchive(parser, args):
4476 """Archives and deletes branches associated with closed changelists."""
4477 parser.add_option(
4478 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004479 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004480 parser.add_option(
4481 '-f', '--force', action='store_true',
4482 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004483 parser.add_option(
4484 '-d', '--dry-run', action='store_true',
4485 help='Skip the branch tagging and removal steps.')
4486 parser.add_option(
4487 '-t', '--notags', action='store_true',
4488 help='Do not tag archived branches. '
4489 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004490
4491 auth.add_auth_options(parser)
4492 options, args = parser.parse_args(args)
4493 if args:
4494 parser.error('Unsupported args: %s' % ' '.join(args))
4495 auth_config = auth.extract_auth_config_from_options(options)
4496
4497 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4498 if not branches:
4499 return 0
4500
vapiera7fbd5a2016-06-16 09:17:49 -07004501 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004502 changes = [Changelist(branchref=b, auth_config=auth_config)
4503 for b in branches.splitlines()]
4504 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4505 statuses = get_cl_statuses(changes,
4506 fine_grained=True,
4507 max_processes=options.maxjobs)
4508 proposal = [(cl.GetBranch(),
4509 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4510 for cl, status in statuses
4511 if status == 'closed']
4512 proposal.sort()
4513
4514 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004515 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004516 return 0
4517
4518 current_branch = GetCurrentBranch()
4519
vapiera7fbd5a2016-06-16 09:17:49 -07004520 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004521 if options.notags:
4522 for next_item in proposal:
4523 print(' ' + next_item[0])
4524 else:
4525 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4526 for next_item in proposal:
4527 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004528
kmarshall9249e012016-08-23 12:02:16 -07004529 # Quit now on precondition failure or if instructed by the user, either
4530 # via an interactive prompt or by command line flags.
4531 if options.dry_run:
4532 print('\nNo changes were made (dry run).\n')
4533 return 0
4534 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004535 print('You are currently on a branch \'%s\' which is associated with a '
4536 'closed codereview issue, so archive cannot proceed. Please '
4537 'checkout another branch and run this command again.' %
4538 current_branch)
4539 return 1
kmarshall9249e012016-08-23 12:02:16 -07004540 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004541 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4542 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004543 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004544 return 1
4545
4546 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004547 if not options.notags:
4548 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004549 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004550
vapiera7fbd5a2016-06-16 09:17:49 -07004551 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004552
4553 return 0
4554
4555
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004556@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004557def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004558 """Show status of changelists.
4559
4560 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004561 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004562 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004563 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004564 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004565 - Magenta in the commit queue
4566 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004567 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004568
4569 Also see 'git cl comments'.
4570 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004571 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004572 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004573 parser.add_option('-f', '--fast', action='store_true',
4574 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004575 parser.add_option(
4576 '-j', '--maxjobs', action='store', type=int,
4577 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004578
4579 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004580 _add_codereview_issue_select_options(
4581 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004582 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004583 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004584 if args:
4585 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004586 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004587
iannuccie53c9352016-08-17 14:40:40 -07004588 if options.issue is not None and not options.field:
4589 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004591 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004592 cl = Changelist(auth_config=auth_config, issue=options.issue,
4593 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004594 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004595 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004596 elif options.field == 'id':
4597 issueid = cl.GetIssue()
4598 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004599 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004600 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004601 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004602 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004603 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004604 elif options.field == 'status':
4605 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004606 elif options.field == 'url':
4607 url = cl.GetIssueURL()
4608 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004609 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004610 return 0
4611
4612 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4613 if not branches:
4614 print('No local branch found.')
4615 return 0
4616
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004617 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004618 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004619 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004620 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004621 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004622 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004623 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004624
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004625 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004626 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4627 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4628 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004629 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004630 c, status = output.next()
4631 branch_statuses[c.GetBranch()] = status
4632 status = branch_statuses.pop(branch)
4633 url = cl.GetIssueURL()
4634 if url and (not status or status == 'error'):
4635 # The issue probably doesn't exist anymore.
4636 url += ' (broken)'
4637
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004638 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004639 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004640 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004641 color = ''
4642 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004643 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004644 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004645 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004646 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004647
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004648
4649 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004650 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004651 print('Current branch: %s' % branch)
4652 for cl in changes:
4653 if cl.GetBranch() == branch:
4654 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004655 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004656 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004657 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004658 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004659 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004660 print('Issue description:')
4661 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004662 return 0
4663
4664
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004665def colorize_CMDstatus_doc():
4666 """To be called once in main() to add colors to git cl status help."""
4667 colors = [i for i in dir(Fore) if i[0].isupper()]
4668
4669 def colorize_line(line):
4670 for color in colors:
4671 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004672 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004673 indent = len(line) - len(line.lstrip(' ')) + 1
4674 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4675 return line
4676
4677 lines = CMDstatus.__doc__.splitlines()
4678 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4679
4680
phajdan.jre328cf92016-08-22 04:12:17 -07004681def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004682 if path == '-':
4683 json.dump(contents, sys.stdout)
4684 else:
4685 with open(path, 'w') as f:
4686 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004687
4688
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004689@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004690@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004692 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693
4694 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004695 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004696 parser.add_option('-r', '--reverse', action='store_true',
4697 help='Lookup the branch(es) for the specified issues. If '
4698 'no issues are specified, all branches with mapped '
4699 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004700 parser.add_option('--json',
4701 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004702 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004703 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004704 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004705
dnj@chromium.org406c4402015-03-03 17:22:28 +00004706 if options.reverse:
4707 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004708 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004709 # Reverse issue lookup.
4710 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004711
4712 git_config = {}
4713 for config in RunGit(['config', '--get-regexp',
4714 r'branch\..*issue']).splitlines():
4715 name, _space, val = config.partition(' ')
4716 git_config[name] = val
4717
dnj@chromium.org406c4402015-03-03 17:22:28 +00004718 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004719 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4720 config_key = _git_branch_config_key(ShortBranchName(branch),
4721 cls.IssueConfigKey())
4722 issue = git_config.get(config_key)
4723 if issue:
4724 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004725 if not args:
4726 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004727 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004728 for issue in args:
4729 if not issue:
4730 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004731 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004732 print('Branch for issue number %s: %s' % (
4733 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004734 if options.json:
4735 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004736 return 0
4737
4738 if len(args) > 0:
4739 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4740 if not issue.valid:
4741 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4742 'or no argument to list it.\n'
4743 'Maybe you want to run git cl status?')
4744 cl = Changelist(codereview=issue.codereview)
4745 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004746 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004747 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004748 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4749 if options.json:
4750 write_json(options.json, {
4751 'issue': cl.GetIssue(),
4752 'issue_url': cl.GetIssueURL(),
4753 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004754 return 0
4755
4756
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004757@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004758def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004759 """Shows or posts review comments for any changelist."""
4760 parser.add_option('-a', '--add-comment', dest='comment',
4761 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004762 parser.add_option('-i', '--issue', dest='issue',
4763 help='review issue id (defaults to current issue). '
4764 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004765 parser.add_option('-m', '--machine-readable', dest='readable',
4766 action='store_false', default=True,
4767 help='output comments in a format compatible with '
4768 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004769 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004770 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004771 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004772 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004773 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004774 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004775 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004776
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004777 issue = None
4778 if options.issue:
4779 try:
4780 issue = int(options.issue)
4781 except ValueError:
4782 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004783 if not options.forced_codereview:
4784 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004785
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004786 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004787 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004788 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004789
4790 if options.comment:
4791 cl.AddComment(options.comment)
4792 return 0
4793
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004794 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4795 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004796 for comment in summary:
4797 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004798 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004799 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004800 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004801 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004802 color = Fore.MAGENTA
4803 else:
4804 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004805 print('\n%s%s %s%s\n%s' % (
4806 color,
4807 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4808 comment.sender,
4809 Fore.RESET,
4810 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4811
smut@google.comc85ac942015-09-15 16:34:43 +00004812 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004813 def pre_serialize(c):
4814 dct = c.__dict__.copy()
4815 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4816 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004817 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004818 return 0
4819
4820
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004821@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004822@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004823def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004824 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004825 parser.add_option('-d', '--display', action='store_true',
4826 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004827 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004828 help='New description to set for this issue (- for stdin, '
4829 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004830 parser.add_option('-f', '--force', action='store_true',
4831 help='Delete any unpublished Gerrit edits for this issue '
4832 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004833
4834 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004835 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004836 options, args = parser.parse_args(args)
4837 _process_codereview_select_options(parser, options)
4838
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004839 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004840 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004841 target_issue_arg = ParseIssueNumberArgument(args[0],
4842 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004843 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004844 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004845
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004846 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004847
martiniss6eda05f2016-06-30 10:18:35 -07004848 kwargs = {
4849 'auth_config': auth_config,
4850 'codereview': options.forced_codereview,
4851 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004852 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004853 if target_issue_arg:
4854 kwargs['issue'] = target_issue_arg.issue
4855 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004856 if target_issue_arg.codereview and not options.forced_codereview:
4857 detected_codereview_from_url = True
4858 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004859
4860 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004861 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004862 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004863 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004864
4865 if detected_codereview_from_url:
4866 logging.info('canonical issue/change URL: %s (type: %s)\n',
4867 cl.GetIssueURL(), target_issue_arg.codereview)
4868
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004869 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004870
smut@google.com34fb6b12015-07-13 20:03:26 +00004871 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004872 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004873 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004874
4875 if options.new_description:
4876 text = options.new_description
4877 if text == '-':
4878 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004879 elif text == '+':
4880 base_branch = cl.GetCommonAncestorWithUpstream()
4881 change = cl.GetChange(base_branch, None, local_description=True)
4882 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004883
4884 description.set_description(text)
4885 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004886 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004887
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004888 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004889 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004890 return 0
4891
4892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004893def CreateDescriptionFromLog(args):
4894 """Pulls out the commit log to use as a base for the CL description."""
4895 log_args = []
4896 if len(args) == 1 and not args[0].endswith('.'):
4897 log_args = [args[0] + '..']
4898 elif len(args) == 1 and args[0].endswith('...'):
4899 log_args = [args[0][:-1]]
4900 elif len(args) == 2:
4901 log_args = [args[0] + '..' + args[1]]
4902 else:
4903 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004904 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004905
4906
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004907@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004908def CMDlint(parser, args):
4909 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004910 parser.add_option('--filter', action='append', metavar='-x,+y',
4911 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004912 auth.add_auth_options(parser)
4913 options, args = parser.parse_args(args)
4914 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004915
4916 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004917 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004918 try:
4919 import cpplint
4920 import cpplint_chromium
4921 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004922 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004923 return 1
4924
4925 # Change the current working directory before calling lint so that it
4926 # shows the correct base.
4927 previous_cwd = os.getcwd()
4928 os.chdir(settings.GetRoot())
4929 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004930 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004931 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4932 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004933 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004934 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004935 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004936
4937 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004938 command = args + files
4939 if options.filter:
4940 command = ['--filter=' + ','.join(options.filter)] + command
4941 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004942
4943 white_regex = re.compile(settings.GetLintRegex())
4944 black_regex = re.compile(settings.GetLintIgnoreRegex())
4945 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4946 for filename in filenames:
4947 if white_regex.match(filename):
4948 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004949 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004950 else:
4951 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4952 extra_check_functions)
4953 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004954 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004955 finally:
4956 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004957 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004958 if cpplint._cpplint_state.error_count != 0:
4959 return 1
4960 return 0
4961
4962
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004963@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004964def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004965 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004966 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004967 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004968 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004969 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004970 parser.add_option('--all', action='store_true',
4971 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004972 parser.add_option('--parallel', action='store_true',
4973 help='Run all tests specified by input_api.RunTests in all '
4974 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004975 auth.add_auth_options(parser)
4976 options, args = parser.parse_args(args)
4977 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004978
sbc@chromium.org71437c02015-04-09 19:29:40 +00004979 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004980 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004981 return 1
4982
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004983 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004984 if args:
4985 base_branch = args[0]
4986 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004987 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004988 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004989
Aaron Gable8076c282017-11-29 14:39:41 -08004990 if options.all:
4991 base_change = cl.GetChange(base_branch, None)
4992 files = [('M', f) for f in base_change.AllFiles()]
4993 change = presubmit_support.GitChange(
4994 base_change.Name(),
4995 base_change.FullDescriptionText(),
4996 base_change.RepositoryRoot(),
4997 files,
4998 base_change.issue,
4999 base_change.patchset,
5000 base_change.author_email,
5001 base_change._upstream)
5002 else:
5003 change = cl.GetChange(base_branch, None)
5004
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00005005 cl.RunHook(
5006 committing=not options.upload,
5007 may_prompt=False,
5008 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04005009 change=change,
5010 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00005011 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005012
5013
tandrii@chromium.org65874e12016-03-04 12:03:02 +00005014def GenerateGerritChangeId(message):
5015 """Returns Ixxxxxx...xxx change id.
5016
5017 Works the same way as
5018 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
5019 but can be called on demand on all platforms.
5020
5021 The basic idea is to generate git hash of a state of the tree, original commit
5022 message, author/committer info and timestamps.
5023 """
5024 lines = []
5025 tree_hash = RunGitSilent(['write-tree'])
5026 lines.append('tree %s' % tree_hash.strip())
5027 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
5028 if code == 0:
5029 lines.append('parent %s' % parent.strip())
5030 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
5031 lines.append('author %s' % author.strip())
5032 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
5033 lines.append('committer %s' % committer.strip())
5034 lines.append('')
5035 # Note: Gerrit's commit-hook actually cleans message of some lines and
5036 # whitespace. This code is not doing this, but it clearly won't decrease
5037 # entropy.
5038 lines.append(message)
5039 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
5040 stdin='\n'.join(lines))
5041 return 'I%s' % change_hash.strip()
5042
5043
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005044def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00005045 """Computes the remote branch ref to use for the CL.
5046
5047 Args:
5048 remote (str): The git remote for the CL.
5049 remote_branch (str): The git remote branch for the CL.
5050 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00005051 """
5052 if not (remote and remote_branch):
5053 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005054
wittman@chromium.org455dc922015-01-26 20:15:50 +00005055 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005056 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00005057 # refs, which are then translated into the remote full symbolic refs
5058 # below.
5059 if '/' not in target_branch:
5060 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
5061 else:
5062 prefix_replacements = (
5063 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
5064 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
5065 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
5066 )
5067 match = None
5068 for regex, replacement in prefix_replacements:
5069 match = re.search(regex, target_branch)
5070 if match:
5071 remote_branch = target_branch.replace(match.group(0), replacement)
5072 break
5073 if not match:
5074 # This is a branch path but not one we recognize; use as-is.
5075 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00005076 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
5077 # Handle the refs that need to land in different refs.
5078 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005079
wittman@chromium.org455dc922015-01-26 20:15:50 +00005080 # Create the true path to the remote branch.
5081 # Does the following translation:
5082 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
5083 # * refs/remotes/origin/master -> refs/heads/master
5084 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
5085 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
5086 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
5087 elif remote_branch.startswith('refs/remotes/%s/' % remote):
5088 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
5089 'refs/heads/')
5090 elif remote_branch.startswith('refs/remotes/branch-heads'):
5091 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01005092
wittman@chromium.org455dc922015-01-26 20:15:50 +00005093 return remote_branch
5094
5095
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005096def cleanup_list(l):
5097 """Fixes a list so that comma separated items are put as individual items.
5098
5099 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
5100 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
5101 """
5102 items = sum((i.split(',') for i in l), [])
5103 stripped_items = (i.strip() for i in items)
5104 return sorted(filter(None, stripped_items))
5105
5106
Aaron Gable4db38df2017-11-03 14:59:07 -07005107@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005108@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005109def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00005110 """Uploads the current changelist to codereview.
5111
5112 Can skip dependency patchset uploads for a branch by running:
5113 git config branch.branch_name.skip-deps-uploads True
5114 To unset run:
5115 git config --unset branch.branch_name.skip-deps-uploads
5116 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02005117
5118 If the name of the checked out branch starts with "bug-" or "fix-" followed by
5119 a bug number, this bug number is automatically populated in the CL
5120 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005121
5122 If subject contains text in square brackets or has "<text>: " prefix, such
5123 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
5124 [git-cl] add support for hashtags
5125 Foo bar: implement foo
5126 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00005127 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00005128 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5129 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00005130 parser.add_option('--bypass-watchlists', action='store_true',
5131 dest='bypass_watchlists',
5132 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005133 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00005134 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005135 parser.add_option('--message', '-m', dest='message',
5136 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07005137 parser.add_option('-b', '--bug',
5138 help='pre-populate the bug number(s) for this issue. '
5139 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07005140 parser.add_option('--message-file', dest='message_file',
5141 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005142 parser.add_option('--title', '-t', dest='title',
5143 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005144 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005145 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005146 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005147 parser.add_option('--tbrs',
5148 action='append', default=[],
5149 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005150 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005151 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005152 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005153 parser.add_option('--hashtag', dest='hashtags',
5154 action='append', default=[],
5155 help=('Gerrit hashtag for new CL; '
5156 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005157 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005158 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005159 parser.add_option('--emulate_svn_auto_props',
5160 '--emulate-svn-auto-props',
5161 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005162 dest="emulate_svn_auto_props",
5163 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005164 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005165 help='tell the commit queue to commit this patchset; '
5166 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005167 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005168 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005169 metavar='TARGET',
5170 help='Apply CL to remote ref TARGET. ' +
5171 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005172 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005173 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005174 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005175 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005176 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005177 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005178 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5179 const='TBR', help='add a set of OWNERS to TBR')
5180 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5181 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005182 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5183 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005184 help='Send the patchset to do a CQ dry run right after '
5185 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005186 parser.add_option('--dependencies', action='store_true',
5187 help='Uploads CLs of all the local branches that depend on '
5188 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005189 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5190 help='Sends your change to the CQ after an approval. Only '
5191 'works on repos that have the Auto-Submit label '
5192 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005193 parser.add_option('--parallel', action='store_true',
5194 help='Run all tests specified by input_api.RunTests in all '
5195 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005196
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005197 parser.add_option('--no-autocc', action='store_true',
5198 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005199 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005200 help='Set the review private. This implies --no-autocc.')
5201
5202 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005203 parser.add_option('--email', default=None,
5204 help='email address to use to connect to Rietveld')
5205
rmistry@google.com2dd99862015-06-22 12:22:18 +00005206 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005207 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005208 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005209 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005210 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005211 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005212
sbc@chromium.org71437c02015-04-09 19:29:40 +00005213 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005214 return 1
5215
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005216 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005217 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005218 options.cc = cleanup_list(options.cc)
5219
tandriib80458a2016-06-23 12:20:07 -07005220 if options.message_file:
5221 if options.message:
5222 parser.error('only one of --message and --message-file allowed.')
5223 options.message = gclient_utils.FileRead(options.message_file)
5224 options.message_file = None
5225
tandrii4d0545a2016-07-06 03:56:49 -07005226 if options.cq_dry_run and options.use_commit_queue:
5227 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5228
Aaron Gableedbc4132017-09-11 13:22:28 -07005229 if options.use_commit_queue:
5230 options.send_mail = True
5231
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005232 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5233 settings.GetIsGerrit()
5234
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005235 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005236 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005237
5238
Francois Dorayd42c6812017-05-30 15:10:20 -04005239@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005240@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005241def CMDsplit(parser, args):
5242 """Splits a branch into smaller branches and uploads CLs.
5243
5244 Creates a branch and uploads a CL for each group of files modified in the
5245 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005246 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005247 the shared OWNERS file.
5248 """
5249 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005250 help="A text file containing a CL description in which "
5251 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005252 parser.add_option("-c", "--comment", dest="comment_file",
5253 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005254 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5255 default=False,
5256 help="List the files and reviewers for each CL that would "
5257 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005258 parser.add_option("--cq-dry-run", action='store_true',
5259 help="If set, will do a cq dry run for each uploaded CL. "
5260 "Please be careful when doing this; more than ~10 CLs "
5261 "has the potential to overload our build "
5262 "infrastructure. Try to upload these not during high "
5263 "load times (usually 11-3 Mountain View time). Email "
5264 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005265 options, _ = parser.parse_args(args)
5266
5267 if not options.description_file:
5268 parser.error('No --description flag specified.')
5269
5270 def WrappedCMDupload(args):
5271 return CMDupload(OptionParser(), args)
5272
5273 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005274 Changelist, WrappedCMDupload, options.dry_run,
5275 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005276
5277
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005278@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005279@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005280def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005281 """DEPRECATED: Used to commit the current changelist via git-svn."""
5282 message = ('git-cl no longer supports committing to SVN repositories via '
5283 'git-svn. You probably want to use `git cl land` instead.')
5284 print(message)
5285 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005286
5287
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005288# Two special branches used by git cl land.
5289MERGE_BRANCH = 'git-cl-commit'
5290CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5291
5292
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005293@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005294@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005295def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005296 """Commits the current changelist via git.
5297
5298 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5299 upstream and closes the issue automatically and atomically.
5300
5301 Otherwise (in case of Rietveld):
5302 Squashes branch into a single commit.
5303 Updates commit message with metadata (e.g. pointer to review).
5304 Pushes the code upstream.
5305 Updates review and closes.
5306 """
5307 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5308 help='bypass upload presubmit hook')
5309 parser.add_option('-m', dest='message',
5310 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005311 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005312 help="force yes to questions (don't prompt)")
5313 parser.add_option('-c', dest='contributor',
5314 help="external contributor for patch (appended to " +
5315 "description and used as author for git). Should be " +
5316 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005317 parser.add_option('--parallel', action='store_true',
5318 help='Run all tests specified by input_api.RunTests in all '
5319 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005320 auth.add_auth_options(parser)
5321 (options, args) = parser.parse_args(args)
5322 auth_config = auth.extract_auth_config_from_options(options)
5323
5324 cl = Changelist(auth_config=auth_config)
5325
Robert Iannucci2e73d432018-03-14 01:10:47 -07005326 if not cl.IsGerrit():
5327 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005328
Robert Iannucci2e73d432018-03-14 01:10:47 -07005329 if options.message:
5330 # This could be implemented, but it requires sending a new patch to
5331 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5332 # Besides, Gerrit has the ability to change the commit message on submit
5333 # automatically, thus there is no need to support this option (so far?).
5334 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005335 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005336 parser.error(
5337 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5338 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5339 'the contributor\'s "name <email>". If you can\'t upload such a '
5340 'commit for review, contact your repository admin and request'
5341 '"Forge-Author" permission.')
5342 if not cl.GetIssue():
5343 DieWithError('You must upload the change first to Gerrit.\n'
5344 ' If you would rather have `git cl land` upload '
5345 'automatically for you, see http://crbug.com/642759')
5346 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005347 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005348
5349
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005350def PushToGitWithAutoRebase(remote, branch, original_description,
5351 git_numberer_enabled, max_attempts=3):
5352 """Pushes current HEAD commit on top of remote's branch.
5353
5354 Attempts to fetch and autorebase on push failures.
5355 Adds git number footers on the fly.
5356
5357 Returns integer code from last command.
5358 """
5359 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5360 code = 0
5361 attempts_left = max_attempts
5362 while attempts_left:
5363 attempts_left -= 1
5364 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5365
5366 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5367 # If fetch fails, retry.
5368 print('Fetching %s/%s...' % (remote, branch))
5369 code, out = RunGitWithCode(
5370 ['retry', 'fetch', remote,
5371 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5372 if code:
5373 print('Fetch failed with exit code %d.' % code)
5374 print(out.strip())
5375 continue
5376
5377 print('Cherry-picking commit on top of latest %s' % branch)
5378 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5379 suppress_stderr=True)
5380 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5381 code, out = RunGitWithCode(['cherry-pick', cherry])
5382 if code:
5383 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5384 'the following files have merge conflicts:' %
5385 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005386 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5387 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005388 print('Please rebase your patch and try again.')
5389 RunGitWithCode(['cherry-pick', '--abort'])
5390 break
5391
5392 commit_desc = ChangeDescription(original_description)
5393 if git_numberer_enabled:
5394 logging.debug('Adding git number footers')
5395 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5396 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5397 branch)
5398 # Ensure timestamps are monotonically increasing.
5399 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5400 _get_committer_timestamp('HEAD'))
5401 _git_amend_head(commit_desc.description, timestamp)
5402
5403 code, out = RunGitWithCode(
5404 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5405 print(out)
5406 if code == 0:
5407 break
5408 if IsFatalPushFailure(out):
5409 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005410 'user.email are correct and you have push access to the repo.\n'
5411 'Hint: run command below to diangose common Git/Gerrit credential '
5412 'problems:\n'
5413 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005414 break
5415 return code
5416
5417
5418def IsFatalPushFailure(push_stdout):
5419 """True if retrying push won't help."""
5420 return '(prohibited by Gerrit)' in push_stdout
5421
5422
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005423@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005424@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005425def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005426 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005427 parser.add_option('-b', dest='newbranch',
5428 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005429 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005430 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005431 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005432 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005433 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005434 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005435 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005436 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005437 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005438 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005439
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005440
5441 group = optparse.OptionGroup(
5442 parser,
5443 'Options for continuing work on the current issue uploaded from a '
5444 'different clone (e.g. different machine). Must be used independently '
5445 'from the other options. No issue number should be specified, and the '
5446 'branch must have an issue number associated with it')
5447 group.add_option('--reapply', action='store_true', dest='reapply',
5448 help='Reset the branch and reapply the issue.\n'
5449 'CAUTION: This will undo any local changes in this '
5450 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005451
5452 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005453 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005454 parser.add_option_group(group)
5455
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005456 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005457 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005458 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005459 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005460 auth_config = auth.extract_auth_config_from_options(options)
5461
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005462 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005463 if options.newbranch:
5464 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005465 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005466 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005467
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005468 cl = Changelist(auth_config=auth_config,
5469 codereview=options.forced_codereview)
5470 if not cl.GetIssue():
5471 parser.error('current branch must have an associated issue')
5472
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005473 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005474 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005475 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005476
5477 RunGit(['reset', '--hard', upstream])
5478 if options.pull:
5479 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005480
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005481 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5482 options.directory)
5483
5484 if len(args) != 1 or not args[0]:
5485 parser.error('Must specify issue number or url')
5486
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005487 target_issue_arg = ParseIssueNumberArgument(args[0],
5488 options.forced_codereview)
5489 if not target_issue_arg.valid:
5490 parser.error('invalid codereview url or CL id')
5491
5492 cl_kwargs = {
5493 'auth_config': auth_config,
5494 'codereview_host': target_issue_arg.hostname,
5495 'codereview': options.forced_codereview,
5496 }
5497 detected_codereview_from_url = False
5498 if target_issue_arg.codereview and not options.forced_codereview:
5499 detected_codereview_from_url = True
5500 cl_kwargs['codereview'] = target_issue_arg.codereview
5501 cl_kwargs['issue'] = target_issue_arg.issue
5502
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005503 # We don't want uncommitted changes mixed up with the patch.
5504 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005505 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005506
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005507 if options.newbranch:
5508 if options.force:
5509 RunGit(['branch', '-D', options.newbranch],
5510 stderr=subprocess2.PIPE, error_ok=True)
5511 RunGit(['new-branch', options.newbranch])
5512
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005513 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005514
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005515 if cl.IsGerrit():
5516 if options.reject:
5517 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005518 if options.directory:
5519 parser.error('--directory is not supported with Gerrit codereview.')
5520
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005521 if detected_codereview_from_url:
5522 print('canonical issue/change URL: %s (type: %s)\n' %
5523 (cl.GetIssueURL(), target_issue_arg.codereview))
5524
5525 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005526 options.nocommit, options.directory,
5527 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005528
5529
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005530def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005531 """Fetches the tree status and returns either 'open', 'closed',
5532 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005533 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005534 if url:
5535 status = urllib2.urlopen(url).read().lower()
5536 if status.find('closed') != -1 or status == '0':
5537 return 'closed'
5538 elif status.find('open') != -1 or status == '1':
5539 return 'open'
5540 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005541 return 'unset'
5542
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005544def GetTreeStatusReason():
5545 """Fetches the tree status from a json url and returns the message
5546 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005547 url = settings.GetTreeStatusUrl()
5548 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005549 connection = urllib2.urlopen(json_url)
5550 status = json.loads(connection.read())
5551 connection.close()
5552 return status['message']
5553
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005554
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005555@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005556def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005557 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005558 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005559 status = GetTreeStatus()
5560 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005561 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005562 return 2
5563
vapiera7fbd5a2016-06-16 09:17:49 -07005564 print('The tree is %s' % status)
5565 print()
5566 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005567 if status != 'open':
5568 return 1
5569 return 0
5570
5571
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005572@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005573def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005574 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005575 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005576 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005577 '-b', '--bot', action='append',
5578 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5579 'times to specify multiple builders. ex: '
5580 '"-b win_rel -b win_layout". See '
5581 'the try server waterfall for the builders name and the tests '
5582 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005583 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005584 '-B', '--bucket', default='',
5585 help=('Buildbucket bucket to send the try requests.'))
5586 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005587 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005588 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005589 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005590 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005591 help='Revision to use for the try job; default: the revision will '
5592 'be determined by the try recipe that builder runs, which usually '
5593 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005594 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005595 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005596 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005597 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005598 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005599 '--category', default='git_cl_try', help='Specify custom build category.')
5600 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005601 '--project',
5602 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005603 'in recipe to determine to which repository or directory to '
5604 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005605 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005606 '-p', '--property', dest='properties', action='append', default=[],
5607 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005608 'key2=value2 etc. The value will be treated as '
5609 'json if decodable, or as string otherwise. '
5610 'NOTE: using this may make your try job not usable for CQ, '
5611 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005612 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005613 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5614 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005615 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005616 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005617 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005618 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005619 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005620 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005621
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005622 if options.master and options.master.startswith('luci.'):
5623 parser.error(
5624 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005625 # Make sure that all properties are prop=value pairs.
5626 bad_params = [x for x in options.properties if '=' not in x]
5627 if bad_params:
5628 parser.error('Got properties with missing "=": %s' % bad_params)
5629
maruel@chromium.org15192402012-09-06 12:38:29 +00005630 if args:
5631 parser.error('Unknown arguments: %s' % args)
5632
Koji Ishii31c14782018-01-08 17:17:33 +09005633 cl = Changelist(auth_config=auth_config, issue=options.issue,
5634 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005635 if not cl.GetIssue():
5636 parser.error('Need to upload first')
5637
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005638 if cl.IsGerrit():
5639 # HACK: warm up Gerrit change detail cache to save on RPCs.
5640 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5641
tandriie113dfd2016-10-11 10:20:12 -07005642 error_message = cl.CannotTriggerTryJobReason()
5643 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005644 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005645
borenet6c0efe62016-10-19 08:13:29 -07005646 if options.bucket and options.master:
5647 parser.error('Only one of --bucket and --master may be used.')
5648
qyearsley1fdfcb62016-10-24 13:22:03 -07005649 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005650
qyearsleydd49f942016-10-28 11:57:22 -07005651 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5652 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005653 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005654 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005655 print('git cl try with no bots now defaults to CQ dry run.')
5656 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5657 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005658
borenet6c0efe62016-10-19 08:13:29 -07005659 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005660 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005661 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005662 'of bot requires an initial job from a parent (usually a builder). '
5663 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005664 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005665 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005666
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005667 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005668 # TODO(tandrii): Checking local patchset against remote patchset is only
5669 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5670 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005671 print('Warning: Codereview server has newer patchsets (%s) than most '
5672 'recent upload from local checkout (%s). Did a previous upload '
5673 'fail?\n'
5674 'By default, git cl try uses the latest patchset from '
5675 'codereview, continuing to use patchset %s.\n' %
5676 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005677
tandrii568043b2016-10-11 07:49:18 -07005678 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005679 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005680 except BuildbucketResponseException as ex:
5681 print('ERROR: %s' % ex)
5682 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005683 return 0
5684
5685
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005686@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005687def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005688 """Prints info about try jobs associated with current CL."""
5689 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005690 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005691 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005692 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005693 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005694 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005695 '--color', action='store_true', default=setup_color.IS_TTY,
5696 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005697 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005698 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5699 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005700 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005701 '--json', help=('Path of JSON output file to write try job results to,'
5702 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005703 parser.add_option_group(group)
5704 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005705 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005706 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005707 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005708 if args:
5709 parser.error('Unrecognized args: %s' % ' '.join(args))
5710
5711 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005712 cl = Changelist(
5713 issue=options.issue, codereview=options.forced_codereview,
5714 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005715 if not cl.GetIssue():
5716 parser.error('Need to upload first')
5717
tandrii221ab252016-10-06 08:12:04 -07005718 patchset = options.patchset
5719 if not patchset:
5720 patchset = cl.GetMostRecentPatchset()
5721 if not patchset:
5722 parser.error('Codereview doesn\'t know about issue %s. '
5723 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005724 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005725 cl.GetIssue())
5726
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005727 # TODO(tandrii): Checking local patchset against remote patchset is only
5728 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5729 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005730 print('Warning: Codereview server has newer patchsets (%s) than most '
5731 'recent upload from local checkout (%s). Did a previous upload '
5732 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005733 'By default, git cl try-results uses the latest patchset from '
5734 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005735 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005736 try:
tandrii221ab252016-10-06 08:12:04 -07005737 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005738 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005739 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005740 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005741 if options.json:
5742 write_try_results_json(options.json, jobs)
5743 else:
5744 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005745 return 0
5746
5747
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005748@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005749@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005750def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005751 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005752 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005753 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005754 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005755
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005756 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005757 if args:
5758 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005759 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005760 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005761 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005762 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005763
5764 # Clear configured merge-base, if there is one.
5765 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005766 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005767 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005768 return 0
5769
5770
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005771@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005772def CMDweb(parser, args):
5773 """Opens the current CL in the web browser."""
5774 _, args = parser.parse_args(args)
5775 if args:
5776 parser.error('Unrecognized args: %s' % ' '.join(args))
5777
5778 issue_url = Changelist().GetIssueURL()
5779 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005780 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005781 return 1
5782
5783 webbrowser.open(issue_url)
5784 return 0
5785
5786
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005787@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005788def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005789 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005790 parser.add_option('-d', '--dry-run', action='store_true',
5791 help='trigger in dry run mode')
5792 parser.add_option('-c', '--clear', action='store_true',
5793 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005794 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005795 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005796 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005797 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005798 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005799 if args:
5800 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005801 if options.dry_run and options.clear:
5802 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5803
iannuccie53c9352016-08-17 14:40:40 -07005804 cl = Changelist(auth_config=auth_config, issue=options.issue,
5805 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005806 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005807 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005808 elif options.dry_run:
5809 state = _CQState.DRY_RUN
5810 else:
5811 state = _CQState.COMMIT
5812 if not cl.GetIssue():
5813 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005814 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005815 return 0
5816
5817
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005818@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005819def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005820 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005821 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005822 auth.add_auth_options(parser)
5823 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005824 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005825 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005826 if args:
5827 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005828 cl = Changelist(auth_config=auth_config, issue=options.issue,
5829 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005830 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005831 if not cl.GetIssue():
5832 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005833 cl.CloseIssue()
5834 return 0
5835
5836
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005837@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005838def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005839 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005840 parser.add_option(
5841 '--stat',
5842 action='store_true',
5843 dest='stat',
5844 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005845 auth.add_auth_options(parser)
5846 options, args = parser.parse_args(args)
5847 auth_config = auth.extract_auth_config_from_options(options)
5848 if args:
5849 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005850
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005851 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005852 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005853 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005854 if not issue:
5855 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005856
Aaron Gablea718c3e2017-08-28 17:47:28 -07005857 base = cl._GitGetBranchConfigValue('last-upload-hash')
5858 if not base:
5859 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5860 if not base:
5861 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5862 revision_info = detail['revisions'][detail['current_revision']]
5863 fetch_info = revision_info['fetch']['http']
5864 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5865 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005866
Aaron Gablea718c3e2017-08-28 17:47:28 -07005867 cmd = ['git', 'diff']
5868 if options.stat:
5869 cmd.append('--stat')
5870 cmd.append(base)
5871 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005872
5873 return 0
5874
5875
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005876@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005877def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005878 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005879 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005880 '--ignore-current',
5881 action='store_true',
5882 help='Ignore the CL\'s current reviewers and start from scratch.')
5883 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005884 '--no-color',
5885 action='store_true',
5886 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005887 parser.add_option(
5888 '--batch',
5889 action='store_true',
5890 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005891 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005892 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005893 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005894
5895 author = RunGit(['config', 'user.email']).strip() or None
5896
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005897 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005898
5899 if args:
5900 if len(args) > 1:
5901 parser.error('Unknown args')
5902 base_branch = args[0]
5903 else:
5904 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005905 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005906
5907 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005908 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5909
5910 if options.batch:
5911 db = owners.Database(change.RepositoryRoot(), file, os.path)
5912 print('\n'.join(db.reviewers_for(affected_files, author)))
5913 return 0
5914
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005915 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005916 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005917 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005918 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005919 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005920 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005921 disable_color=options.no_color,
5922 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005923
5924
Aiden Bennerc08566e2018-10-03 17:52:42 +00005925def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005926 """Generates a diff command."""
5927 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005928 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5929
5930 if not allow_prefix:
5931 diff_cmd += ['--no-prefix']
5932
5933 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005934
5935 if args:
5936 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005937 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005938 diff_cmd.append(arg)
5939 else:
5940 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005941
5942 return diff_cmd
5943
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005944
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005945def MatchingFileType(file_name, extensions):
5946 """Returns true if the file name ends with one of the given extensions."""
5947 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005948
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005949
enne@chromium.org555cfe42014-01-29 18:21:39 +00005950@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005951@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005952def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005953 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005954 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005955 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005956 parser.add_option('--full', action='store_true',
5957 help='Reformat the full content of all touched files')
5958 parser.add_option('--dry-run', action='store_true',
5959 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005960 parser.add_option('--python', action='store_true',
5961 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005962 parser.add_option('--js', action='store_true',
5963 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005964 parser.add_option('--diff', action='store_true',
5965 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005966 parser.add_option('--presubmit', action='store_true',
5967 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005968 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005969
Daniel Chengc55eecf2016-12-30 03:11:02 -08005970 # Normalize any remaining args against the current path, so paths relative to
5971 # the current directory are still resolved as expected.
5972 args = [os.path.join(os.getcwd(), arg) for arg in args]
5973
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005974 # git diff generates paths against the root of the repository. Change
5975 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005976 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005977 if rel_base_path:
5978 os.chdir(rel_base_path)
5979
digit@chromium.org29e47272013-05-17 17:01:46 +00005980 # Grab the merge-base commit, i.e. the upstream commit of the current
5981 # branch when it was created or the last time it was rebased. This is
5982 # to cover the case where the user may have called "git fetch origin",
5983 # moving the origin branch to a newer commit, but hasn't rebased yet.
5984 upstream_commit = None
5985 cl = Changelist()
5986 upstream_branch = cl.GetUpstreamBranch()
5987 if upstream_branch:
5988 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5989 upstream_commit = upstream_commit.strip()
5990
5991 if not upstream_commit:
5992 DieWithError('Could not find base commit for this branch. '
5993 'Are you in detached state?')
5994
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005995 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5996 diff_output = RunGit(changed_files_cmd)
5997 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005998 # Filter out files deleted by this CL
5999 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006000
Christopher Lamc5ba6922017-01-24 11:19:14 +11006001 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00006002 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006003
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006004 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
6005 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
6006 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006007 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006008
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00006009 top_dir = os.path.normpath(
6010 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
6011
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006012 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6013 # formatted. This is used to block during the presubmit.
6014 return_value = 0
6015
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006016 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006017 # Locate the clang-format binary in the checkout
6018 try:
6019 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07006020 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006021 DieWithError(e)
6022
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006023 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006024 cmd = [clang_format_tool]
6025 if not opts.dry_run and not opts.diff:
6026 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006027 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006028 if opts.diff:
6029 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006030 else:
6031 env = os.environ.copy()
6032 env['PATH'] = str(os.path.dirname(clang_format_tool))
6033 try:
6034 script = clang_format.FindClangFormatScriptInChromiumTree(
6035 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006036 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006037 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006038
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006039 cmd = [sys.executable, script, '-p0']
6040 if not opts.dry_run and not opts.diff:
6041 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006042
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006043 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6044 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006045
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006046 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6047 if opts.diff:
6048 sys.stdout.write(stdout)
6049 if opts.dry_run and len(stdout) > 0:
6050 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006051
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006052 # Similar code to above, but using yapf on .py files rather than clang-format
6053 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00006054 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006055 yapf_tool = gclient_utils.FindExecutable('yapf')
6056 if yapf_tool is None:
6057 DieWithError('yapf not found in PATH')
6058
Aiden Bennerc08566e2018-10-03 17:52:42 +00006059 # If we couldn't find a yapf file we'll default to the chromium style
6060 # specified in depot_tools.
6061 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6062 chromium_default_yapf_style = os.path.join(depot_tools_path,
6063 YAPF_CONFIG_FILENAME)
6064
6065 # Note: yapf still seems to fix indentation of the entire file
6066 # even if line ranges are specified.
6067 # See https://github.com/google/yapf/issues/499
6068 if not opts.full:
6069 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
6070
6071 # Used for caching.
6072 yapf_configs = {}
6073 for f in python_diff_files:
6074 # Find the yapf style config for the current file, defaults to depot
6075 # tools default.
6076 yapf_config = _FindYapfConfigFile(
6077 os.path.abspath(f), yapf_configs, top_dir,
6078 chromium_default_yapf_style)
6079
6080 cmd = [yapf_tool, '--style', yapf_config, f]
6081
6082 has_formattable_lines = False
6083 if not opts.full:
6084 # Only run yapf over changed line ranges.
6085 for diff_start, diff_len in py_line_diffs[f]:
6086 diff_end = diff_start + diff_len - 1
6087 # Yapf errors out if diff_end < diff_start but this
6088 # is a valid line range diff for a removal.
6089 if diff_end >= diff_start:
6090 has_formattable_lines = True
6091 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6092 # If all line diffs were removals we have nothing to format.
6093 if not has_formattable_lines:
6094 continue
6095
6096 if opts.diff or opts.dry_run:
6097 cmd += ['--diff']
6098 # Will return non-zero exit code if non-empty diff.
6099 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
6100 if opts.diff:
6101 sys.stdout.write(stdout)
6102 elif len(stdout) > 0:
6103 return_value = 2
6104 else:
6105 cmd += ['-i']
6106 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006107
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006108 # Dart's formatter does not have the nice property of only operating on
6109 # modified chunks, so hard code full.
6110 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006111 try:
6112 command = [dart_format.FindDartFmtToolInChromiumTree()]
6113 if not opts.dry_run and not opts.diff:
6114 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006115 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006116
ppi@chromium.org6593d932016-03-03 15:41:15 +00006117 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006118 if opts.dry_run and stdout:
6119 return_value = 2
6120 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006121 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6122 'found in this checkout. Files in other languages are still '
6123 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006124
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006125 # Format GN build files. Always run on full build files for canonical form.
6126 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006127 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006128 if opts.dry_run or opts.diff:
6129 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006130 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006131 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6132 shell=sys.platform == 'win32',
6133 cwd=top_dir)
6134 if opts.dry_run and gn_ret == 2:
6135 return_value = 2 # Not formatted.
6136 elif opts.diff and gn_ret == 2:
6137 # TODO this should compute and print the actual diff.
6138 print("This change has GN build file diff for " + gn_diff_file)
6139 elif gn_ret != 0:
6140 # For non-dry run cases (and non-2 return values for dry-run), a
6141 # nonzero error code indicates a failure, probably because the file
6142 # doesn't parse.
6143 DieWithError("gn format failed on " + gn_diff_file +
6144 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006145
Ilya Shermane081cbe2017-08-15 17:51:04 -07006146 # Skip the metrics formatting from the global presubmit hook. These files have
6147 # a separate presubmit hook that issues an error if the files need formatting,
6148 # whereas the top-level presubmit script merely issues a warning. Formatting
6149 # these files is somewhat slow, so it's important not to duplicate the work.
6150 if not opts.presubmit:
6151 for xml_dir in GetDirtyMetricsDirs(diff_files):
6152 tool_dir = os.path.join(top_dir, xml_dir)
6153 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6154 if opts.dry_run or opts.diff:
6155 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006156 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006157 if opts.diff:
6158 sys.stdout.write(stdout)
6159 if opts.dry_run and stdout:
6160 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006161
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006162 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006163
Steven Holte2e664bf2017-04-21 13:10:47 -07006164def GetDirtyMetricsDirs(diff_files):
6165 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6166 metrics_xml_dirs = [
6167 os.path.join('tools', 'metrics', 'actions'),
6168 os.path.join('tools', 'metrics', 'histograms'),
6169 os.path.join('tools', 'metrics', 'rappor'),
6170 os.path.join('tools', 'metrics', 'ukm')]
6171 for xml_dir in metrics_xml_dirs:
6172 if any(file.startswith(xml_dir) for file in xml_diff_files):
6173 yield xml_dir
6174
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006175
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006176@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006177@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006178def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006179 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006180 _, args = parser.parse_args(args)
6181
6182 if len(args) != 1:
6183 parser.print_help()
6184 return 1
6185
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006186 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006187 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006188 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006189
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006190 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006191
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006192 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006193 output = RunGit(['config', '--local', '--get-regexp',
6194 r'branch\..*\.%s' % issueprefix],
6195 error_ok=True)
6196 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006197 if issue == target_issue:
6198 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006199
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006200 branches = []
6201 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006202 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006203 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006204 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006205 return 1
6206 if len(branches) == 1:
6207 RunGit(['checkout', branches[0]])
6208 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006209 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006210 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006211 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006212 which = raw_input('Choose by index: ')
6213 try:
6214 RunGit(['checkout', branches[int(which)]])
6215 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006216 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006217 return 1
6218
6219 return 0
6220
6221
maruel@chromium.org29404b52014-09-08 22:58:00 +00006222def CMDlol(parser, args):
6223 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006224 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006225 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6226 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6227 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006228 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006229 return 0
6230
6231
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006232class OptionParser(optparse.OptionParser):
6233 """Creates the option parse and add --verbose support."""
6234 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006235 optparse.OptionParser.__init__(
6236 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006237 self.add_option(
6238 '-v', '--verbose', action='count', default=0,
6239 help='Use 2 times for more debugging info')
6240
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006241 def parse_args(self, args=None, _values=None):
6242 # Create an optparse.Values object that will store only the actual passed
6243 # options, without the defaults.
6244 actual_options = optparse.Values()
6245 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6246 # Create an optparse.Values object with the default options.
6247 options = optparse.Values(self.get_default_values().__dict__)
6248 # Update it with the options passed by the user.
6249 options._update_careful(actual_options.__dict__)
6250 # Store the options passed by the user in an _actual_options attribute.
6251 # We store only the keys, and not the values, since the values can contain
6252 # arbitrary information, which might be PII.
6253 metrics.collector.add('arguments', actual_options.__dict__.keys())
6254
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006255 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006256 logging.basicConfig(
6257 level=levels[min(options.verbose, len(levels) - 1)],
6258 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6259 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006260
6261 # TODO(crbug.com/881860): Remove.
6262 # Clear the log after each git-cl run by setting mode='w'.
6263 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
6264 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
6265
6266 GERRIT_ERR_LOGGER.addHandler(handler)
6267 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
6268 # Don't propagate to root logger, so that logs are not printed.
6269 GERRIT_ERR_LOGGER.propagate = 0
6270
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006271 return options, args
6272
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006274def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006275 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006276 print('\nYour python version %s is unsupported, please upgrade.\n' %
6277 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006278 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006279
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006280 # Reload settings.
6281 global settings
6282 settings = Settings()
6283
Edward Lemurad463c92018-07-25 21:31:23 +00006284 if not metrics.DISABLE_METRICS_COLLECTION:
6285 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006286 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006287 dispatcher = subcommand.CommandDispatcher(__name__)
6288 try:
6289 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006290 except auth.AuthenticationError as e:
6291 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006292 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006293 if e.code != 500:
6294 raise
6295 DieWithError(
6296 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6297 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006298 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006299
6300
6301if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006302 # These affect sys.stdout so do it outside of main() to simplify mocks in
6303 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006304 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006305 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006306 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006307 sys.exit(main(sys.argv[1:]))