blob: 93e21989d41cd3960c0995b5cb1b03044d5fb299 [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.
Edward Lemur47faa062018-10-11 19:46:02 +00003173 # Clear the log after each git-cl upload run by setting mode='w'.
3174 handler = logging.FileHandler(gerrit_util.GERRIT_ERR_LOG_FILE, mode='w')
3175 handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
3176
3177 GERRIT_ERR_LOGGER.addHandler(handler)
3178 GERRIT_ERR_LOGGER.setLevel(logging.INFO)
3179 # Don't propagate to root logger, so that logs are not printed.
3180 GERRIT_ERR_LOGGER.propagate = 0
3181
Edward Lemur83bd7f42018-10-10 00:14:21 +00003182 # Get interesting headers from git push, to be displayed to the user if
3183 # subsequent Gerrit RPC calls fail.
3184 env = os.environ.copy()
3185 env['GIT_CURL_VERBOSE'] = '1'
3186 class FilterHeaders(object):
3187 """Filter git push headers and store them in a file.
3188
3189 Regular git push output is printed directly.
3190 """
3191
3192 def __init__(self):
3193 # The output from git push that we want to store in a file.
3194 self._output = ''
3195 # Keeps track of whether the current line is part of a request header.
3196 self._on_header = False
3197 # Keeps track of repeated empty lines, which mark the end of a request
3198 # header.
3199 self._last_line_empty = False
3200
3201 def __call__(self, line):
3202 """Handle a single line of git push output."""
3203 if not line:
3204 # Two consecutive empty lines mark the end of a header.
3205 if self._last_line_empty:
3206 self._on_header = False
3207 self._last_line_empty = True
3208 return
3209
3210 self._last_line_empty = False
3211 # A line starting with '>' marks the beggining of a request header.
3212 if line[0] == '>':
3213 self._on_header = True
3214 GERRIT_ERR_LOGGER.info(line)
3215 # Lines not starting with '*' or '<', and not part of a request header
3216 # should be displayed to the user.
3217 elif line[0] not in '*<' and not self._on_header:
3218 print(line)
3219 # Flush after every line: useful for seeing progress when running as
3220 # recipe.
3221 sys.stdout.flush()
3222 # Filter out the cookie and authorization headers.
3223 elif ('cookie: ' not in line.lower()
3224 and 'authorization: ' not in line.lower()):
3225 GERRIT_ERR_LOGGER.info(line)
3226
3227 filter_fn = FilterHeaders()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003228 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00003229 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemur83bd7f42018-10-10 00:14:21 +00003230 print_stdout=False,
3231 filter_fn=filter_fn,
3232 env=env)
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003233 except subprocess2.CalledProcessError:
3234 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003235 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003236 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003237 'credential problems:\n'
3238 ' git cl creds-check\n',
3239 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003240
3241 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003242 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003243 change_numbers = [m.group(1)
3244 for m in map(regex.match, push_stdout.splitlines())
3245 if m]
3246 if len(change_numbers) != 1:
3247 DieWithError(
3248 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003249 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003250 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003251 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003252
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003253 reviewers = sorted(change_desc.get_reviewers())
3254
tandrii88189772016-09-29 04:29:57 -07003255 # Add cc's from the CC_LIST and --cc flag (if any).
Sergiy Byelozyorovaaf2cc02018-09-24 18:02:28 +00003256 if not options.private and not options.no_autocc:
Aaron Gabled1052492017-05-15 15:05:34 -07003257 cc = self.GetCCList().split(',')
3258 else:
3259 cc = []
tandrii88189772016-09-29 04:29:57 -07003260 if options.cc:
3261 cc.extend(options.cc)
3262 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003263 if change_desc.get_cced():
3264 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003265
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003266 if self.GetIssue():
3267 # GetIssue() is not set in case of non-squash uploads according to tests.
3268 # TODO(agable): non-squash uploads in git cl should be removed.
3269 gerrit_util.AddReviewers(
3270 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003271 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003272 reviewers, cc,
3273 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003274
Aaron Gablefd238082017-06-07 13:42:34 -07003275 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003276 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3277 score = 1
3278 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3279 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3280 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003281 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003282 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003283 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003284 msg='Self-approving for TBR',
3285 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003286
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003287 return 0
3288
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003289 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3290 change_desc):
3291 """Computes parent of the generated commit to be uploaded to Gerrit.
3292
3293 Returns revision or a ref name.
3294 """
3295 if custom_cl_base:
3296 # Try to avoid creating additional unintended CLs when uploading, unless
3297 # user wants to take this risk.
3298 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3299 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3300 local_ref_of_target_remote])
3301 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003302 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003303 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3304 'If you proceed with upload, more than 1 CL may be created by '
3305 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3306 'If you are certain that specified base `%s` has already been '
3307 'uploaded to Gerrit as another CL, you may proceed.\n' %
3308 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3309 if not force:
3310 confirm_or_exit(
3311 'Do you take responsibility for cleaning up potential mess '
3312 'resulting from proceeding with upload?',
3313 action='upload')
3314 return custom_cl_base
3315
Aaron Gablef97e33d2017-03-30 15:44:27 -07003316 if remote != '.':
3317 return self.GetCommonAncestorWithUpstream()
3318
3319 # If our upstream branch is local, we base our squashed commit on its
3320 # squashed version.
3321 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3322
Aaron Gablef97e33d2017-03-30 15:44:27 -07003323 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003324 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003325
3326 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003327 # TODO(tandrii): consider checking parent change in Gerrit and using its
3328 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3329 # the tree hash of the parent branch. The upside is less likely bogus
3330 # requests to reupload parent change just because it's uploadhash is
3331 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003332 parent = RunGit(['config',
3333 'branch.%s.gerritsquashhash' % upstream_branch_name],
3334 error_ok=True).strip()
3335 # Verify that the upstream branch has been uploaded too, otherwise
3336 # Gerrit will create additional CLs when uploading.
3337 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3338 RunGitSilent(['rev-parse', parent + ':'])):
3339 DieWithError(
3340 '\nUpload upstream branch %s first.\n'
3341 'It is likely that this branch has been rebased since its last '
3342 'upload, so you just need to upload it again.\n'
3343 '(If you uploaded it with --no-squash, then branch dependencies '
3344 'are not supported, and you should reupload with --squash.)'
3345 % upstream_branch_name,
3346 change_desc)
3347 return parent
3348
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003349 def _AddChangeIdToCommitMessage(self, options, args):
3350 """Re-commits using the current message, assumes the commit hook is in
3351 place.
3352 """
3353 log_desc = options.message or CreateDescriptionFromLog(args)
3354 git_command = ['commit', '--amend', '-m', log_desc]
3355 RunGit(git_command)
3356 new_log_desc = CreateDescriptionFromLog(args)
3357 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003358 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003359 return new_log_desc
3360 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003361 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003362
Ravi Mistry31e7d562018-04-02 12:53:57 -04003363 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3364 """Sets labels on the change based on the provided flags."""
3365 labels = {}
3366 notify = None;
3367 if enable_auto_submit:
3368 labels['Auto-Submit'] = 1
3369 if use_commit_queue:
3370 labels['Commit-Queue'] = 2
3371 elif cq_dry_run:
3372 labels['Commit-Queue'] = 1
3373 notify = False
3374 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003375 gerrit_util.SetReview(
3376 self._GetGerritHost(),
3377 self._GerritChangeIdentifier(),
3378 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003379
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003380 def SetCQState(self, new_state):
3381 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003382 vote_map = {
3383 _CQState.NONE: 0,
3384 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003385 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003386 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003387 labels = {'Commit-Queue': vote_map[new_state]}
3388 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003389 gerrit_util.SetReview(
3390 self._GetGerritHost(), self._GerritChangeIdentifier(),
3391 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003392
tandriie113dfd2016-10-11 10:20:12 -07003393 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003394 try:
3395 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003396 except GerritChangeNotExists:
3397 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003398
3399 if data['status'] in ('ABANDONED', 'MERGED'):
3400 return 'CL %s is closed' % self.GetIssue()
3401
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003402 def GetTryJobProperties(self, patchset=None):
3403 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003404 data = self._GetChangeDetail(['ALL_REVISIONS'])
3405 patchset = int(patchset or self.GetPatchset())
3406 assert patchset
3407 revision_data = None # Pylint wants it to be defined.
3408 for revision_data in data['revisions'].itervalues():
3409 if int(revision_data['_number']) == patchset:
3410 break
3411 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003412 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003413 (patchset, self.GetIssue()))
3414 return {
3415 'patch_issue': self.GetIssue(),
3416 'patch_set': patchset or self.GetPatchset(),
3417 'patch_project': data['project'],
3418 'patch_storage': 'gerrit',
3419 'patch_ref': revision_data['fetch']['http']['ref'],
3420 'patch_repository_url': revision_data['fetch']['http']['url'],
3421 'patch_gerrit_url': self.GetCodereviewServer(),
3422 }
tandriie113dfd2016-10-11 10:20:12 -07003423
tandriide281ae2016-10-12 06:02:30 -07003424 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003425 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003426
Edward Lemur707d70b2018-02-07 00:50:14 +01003427 def GetReviewers(self):
3428 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3429 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3430
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003431
3432_CODEREVIEW_IMPLEMENTATIONS = {
3433 'rietveld': _RietveldChangelistImpl,
3434 'gerrit': _GerritChangelistImpl,
3435}
3436
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003437
iannuccie53c9352016-08-17 14:40:40 -07003438def _add_codereview_issue_select_options(parser, extra=""):
3439 _add_codereview_select_options(parser)
3440
3441 text = ('Operate on this issue number instead of the current branch\'s '
3442 'implicit issue.')
3443 if extra:
3444 text += ' '+extra
3445 parser.add_option('-i', '--issue', type=int, help=text)
3446
3447
3448def _process_codereview_issue_select_options(parser, options):
3449 _process_codereview_select_options(parser, options)
3450 if options.issue is not None and not options.forced_codereview:
3451 parser.error('--issue must be specified with either --rietveld or --gerrit')
3452
3453
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003454def _add_codereview_select_options(parser):
3455 """Appends --gerrit and --rietveld options to force specific codereview."""
3456 parser.codereview_group = optparse.OptionGroup(
3457 parser, 'EXPERIMENTAL! Codereview override options')
3458 parser.add_option_group(parser.codereview_group)
3459 parser.codereview_group.add_option(
3460 '--gerrit', action='store_true',
3461 help='Force the use of Gerrit for codereview')
3462 parser.codereview_group.add_option(
3463 '--rietveld', action='store_true',
3464 help='Force the use of Rietveld for codereview')
3465
3466
3467def _process_codereview_select_options(parser, options):
3468 if options.gerrit and options.rietveld:
3469 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3470 options.forced_codereview = None
3471 if options.gerrit:
3472 options.forced_codereview = 'gerrit'
3473 elif options.rietveld:
3474 options.forced_codereview = 'rietveld'
3475
3476
tandriif9aefb72016-07-01 09:06:51 -07003477def _get_bug_line_values(default_project, bugs):
3478 """Given default_project and comma separated list of bugs, yields bug line
3479 values.
3480
3481 Each bug can be either:
3482 * a number, which is combined with default_project
3483 * string, which is left as is.
3484
3485 This function may produce more than one line, because bugdroid expects one
3486 project per line.
3487
3488 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3489 ['v8:123', 'chromium:789']
3490 """
3491 default_bugs = []
3492 others = []
3493 for bug in bugs.split(','):
3494 bug = bug.strip()
3495 if bug:
3496 try:
3497 default_bugs.append(int(bug))
3498 except ValueError:
3499 others.append(bug)
3500
3501 if default_bugs:
3502 default_bugs = ','.join(map(str, default_bugs))
3503 if default_project:
3504 yield '%s:%s' % (default_project, default_bugs)
3505 else:
3506 yield default_bugs
3507 for other in sorted(others):
3508 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3509 yield other
3510
3511
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003512class ChangeDescription(object):
3513 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003514 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003515 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003516 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003517 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003518 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3519 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3520 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3521 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003522
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003523 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003524 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003525
agable@chromium.org42c20792013-09-12 17:34:49 +00003526 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003527 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003528 return '\n'.join(self._description_lines)
3529
3530 def set_description(self, desc):
3531 if isinstance(desc, basestring):
3532 lines = desc.splitlines()
3533 else:
3534 lines = [line.rstrip() for line in desc]
3535 while lines and not lines[0]:
3536 lines.pop(0)
3537 while lines and not lines[-1]:
3538 lines.pop(-1)
3539 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003540
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003541 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3542 """Rewrites the R=/TBR= line(s) as a single line each.
3543
3544 Args:
3545 reviewers (list(str)) - list of additional emails to use for reviewers.
3546 tbrs (list(str)) - list of additional emails to use for TBRs.
3547 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3548 the change that are missing OWNER coverage. If this is not None, you
3549 must also pass a value for `change`.
3550 change (Change) - The Change that should be used for OWNERS lookups.
3551 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003552 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003553 assert isinstance(tbrs, list), tbrs
3554
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003555 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003556 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003557
3558 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003559 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003560
3561 reviewers = set(reviewers)
3562 tbrs = set(tbrs)
3563 LOOKUP = {
3564 'TBR': tbrs,
3565 'R': reviewers,
3566 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003567
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003568 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003569 regexp = re.compile(self.R_LINE)
3570 matches = [regexp.match(line) for line in self._description_lines]
3571 new_desc = [l for i, l in enumerate(self._description_lines)
3572 if not matches[i]]
3573 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003574
agable@chromium.org42c20792013-09-12 17:34:49 +00003575 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003576
3577 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003578 for match in matches:
3579 if not match:
3580 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003581 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3582
3583 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003584 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003585 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003586 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003587 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003588 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003589 LOOKUP[add_owners_to].update(
3590 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003591
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003592 # If any folks ended up in both groups, remove them from tbrs.
3593 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003594
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003595 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3596 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003597
3598 # Put the new lines in the description where the old first R= line was.
3599 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3600 if 0 <= line_loc < len(self._description_lines):
3601 if new_tbr_line:
3602 self._description_lines.insert(line_loc, new_tbr_line)
3603 if new_r_line:
3604 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003605 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003606 if new_r_line:
3607 self.append_footer(new_r_line)
3608 if new_tbr_line:
3609 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003610
Aaron Gable3a16ed12017-03-23 10:51:55 -07003611 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003612 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003613 self.set_description([
3614 '# Enter a description of the change.',
3615 '# This will be displayed on the codereview site.',
3616 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003617 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003618 '--------------------',
3619 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003620
agable@chromium.org42c20792013-09-12 17:34:49 +00003621 regexp = re.compile(self.BUG_LINE)
3622 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003623 prefix = settings.GetBugPrefix()
3624 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003625 if git_footer:
3626 self.append_footer('Bug: %s' % ', '.join(values))
3627 else:
3628 for value in values:
3629 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003630
agable@chromium.org42c20792013-09-12 17:34:49 +00003631 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003632 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003633 if not content:
3634 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003635 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003636
Bruce Dawson2377b012018-01-11 16:46:49 -08003637 # Strip off comments and default inserted "Bug:" line.
3638 clean_lines = [line.rstrip() for line in lines if not
3639 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003640 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003641 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003642 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003643
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003644 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003645 """Adds a footer line to the description.
3646
3647 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3648 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3649 that Gerrit footers are always at the end.
3650 """
3651 parsed_footer_line = git_footers.parse_footer(line)
3652 if parsed_footer_line:
3653 # Line is a gerrit footer in the form: Footer-Key: any value.
3654 # Thus, must be appended observing Gerrit footer rules.
3655 self.set_description(
3656 git_footers.add_footer(self.description,
3657 key=parsed_footer_line[0],
3658 value=parsed_footer_line[1]))
3659 return
3660
3661 if not self._description_lines:
3662 self._description_lines.append(line)
3663 return
3664
3665 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3666 if gerrit_footers:
3667 # git_footers.split_footers ensures that there is an empty line before
3668 # actual (gerrit) footers, if any. We have to keep it that way.
3669 assert top_lines and top_lines[-1] == ''
3670 top_lines, separator = top_lines[:-1], top_lines[-1:]
3671 else:
3672 separator = [] # No need for separator if there are no gerrit_footers.
3673
3674 prev_line = top_lines[-1] if top_lines else ''
3675 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3676 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3677 top_lines.append('')
3678 top_lines.append(line)
3679 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003680
tandrii99a72f22016-08-17 14:33:24 -07003681 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003682 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003683 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003684 reviewers = [match.group(2).strip()
3685 for match in matches
3686 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003687 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003688
bradnelsond975b302016-10-23 12:20:23 -07003689 def get_cced(self):
3690 """Retrieves the list of reviewers."""
3691 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3692 cced = [match.group(2).strip() for match in matches if match]
3693 return cleanup_list(cced)
3694
Nodir Turakulov23b82142017-11-16 11:04:25 -08003695 def get_hash_tags(self):
3696 """Extracts and sanitizes a list of Gerrit hashtags."""
3697 subject = (self._description_lines or ('',))[0]
3698 subject = re.sub(
3699 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3700
3701 tags = []
3702 start = 0
3703 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3704 while True:
3705 m = bracket_exp.match(subject, start)
3706 if not m:
3707 break
3708 tags.append(self.sanitize_hash_tag(m.group(1)))
3709 start = m.end()
3710
3711 if not tags:
3712 # Try "Tag: " prefix.
3713 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3714 if m:
3715 tags.append(self.sanitize_hash_tag(m.group(1)))
3716 return tags
3717
3718 @classmethod
3719 def sanitize_hash_tag(cls, tag):
3720 """Returns a sanitized Gerrit hash tag.
3721
3722 A sanitized hashtag can be used as a git push refspec parameter value.
3723 """
3724 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3725
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003726 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3727 """Updates this commit description given the parent.
3728
3729 This is essentially what Gnumbd used to do.
3730 Consult https://goo.gl/WMmpDe for more details.
3731 """
3732 assert parent_msg # No, orphan branch creation isn't supported.
3733 assert parent_hash
3734 assert dest_ref
3735 parent_footer_map = git_footers.parse_footers(parent_msg)
3736 # This will also happily parse svn-position, which GnumbD is no longer
3737 # supporting. While we'd generate correct footers, the verifier plugin
3738 # installed in Gerrit will block such commit (ie git push below will fail).
3739 parent_position = git_footers.get_position(parent_footer_map)
3740
3741 # Cherry-picks may have last line obscuring their prior footers,
3742 # from git_footers perspective. This is also what Gnumbd did.
3743 cp_line = None
3744 if (self._description_lines and
3745 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3746 cp_line = self._description_lines.pop()
3747
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003748 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003749
3750 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3751 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003752 for i, line in enumerate(footer_lines):
3753 k, v = git_footers.parse_footer(line) or (None, None)
3754 if k and k.startswith('Cr-'):
3755 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003756
3757 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003758 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003759 if parent_position[0] == dest_ref:
3760 # Same branch as parent.
3761 number = int(parent_position[1]) + 1
3762 else:
3763 number = 1 # New branch, and extra lineage.
3764 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3765 int(parent_position[1])))
3766
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003767 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3768 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003769
3770 self._description_lines = top_lines
3771 if cp_line:
3772 self._description_lines.append(cp_line)
3773 if self._description_lines[-1] != '':
3774 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003775 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003776
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003777
Aaron Gablea1bab272017-04-11 16:38:18 -07003778def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003779 """Retrieves the reviewers that approved a CL from the issue properties with
3780 messages.
3781
3782 Note that the list may contain reviewers that are not committer, thus are not
3783 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003784
3785 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003786 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003787 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003788 return sorted(
3789 set(
3790 message['sender']
3791 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003792 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003793 )
3794 )
3795
3796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003797def FindCodereviewSettingsFile(filename='codereview.settings'):
3798 """Finds the given file starting in the cwd and going up.
3799
3800 Only looks up to the top of the repository unless an
3801 'inherit-review-settings-ok' file exists in the root of the repository.
3802 """
3803 inherit_ok_file = 'inherit-review-settings-ok'
3804 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003805 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3807 root = '/'
3808 while True:
3809 if filename in os.listdir(cwd):
3810 if os.path.isfile(os.path.join(cwd, filename)):
3811 return open(os.path.join(cwd, filename))
3812 if cwd == root:
3813 break
3814 cwd = os.path.dirname(cwd)
3815
3816
3817def LoadCodereviewSettingsFromFile(fileobj):
3818 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003819 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003821 def SetProperty(name, setting, unset_error_ok=False):
3822 fullname = 'rietveld.' + name
3823 if setting in keyvals:
3824 RunGit(['config', fullname, keyvals[setting]])
3825 else:
3826 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3827
tandrii48df5812016-10-17 03:55:37 -07003828 if not keyvals.get('GERRIT_HOST', False):
3829 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 # Only server setting is required. Other settings can be absent.
3831 # In that case, we ignore errors raised during option deletion attempt.
3832 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003833 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003834 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3835 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003836 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003837 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3838 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003839 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003840 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3841 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003842
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003843 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003844 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003845
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003846 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003847 RunGit(['config', 'gerrit.squash-uploads',
3848 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003849
tandrii@chromium.org28253532016-04-14 13:46:56 +00003850 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003851 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003852 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003854 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003855 # should be of the form
3856 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3857 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3859 keyvals['ORIGIN_URL_CONFIG']])
3860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003861
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003862def urlretrieve(source, destination):
3863 """urllib is broken for SSL connections via a proxy therefore we
3864 can't use urllib.urlretrieve()."""
3865 with open(destination, 'w') as f:
3866 f.write(urllib2.urlopen(source).read())
3867
3868
ukai@chromium.org712d6102013-11-27 00:52:58 +00003869def hasSheBang(fname):
3870 """Checks fname is a #! script."""
3871 with open(fname) as f:
3872 return f.read(2).startswith('#!')
3873
3874
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003875# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3876def DownloadHooks(*args, **kwargs):
3877 pass
3878
3879
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003880def DownloadGerritHook(force):
3881 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003882
3883 Args:
3884 force: True to update hooks. False to install hooks if not present.
3885 """
3886 if not settings.GetIsGerrit():
3887 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003888 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003889 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3890 if not os.access(dst, os.X_OK):
3891 if os.path.exists(dst):
3892 if not force:
3893 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003894 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003895 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003896 if not hasSheBang(dst):
3897 DieWithError('Not a script: %s\n'
3898 'You need to download from\n%s\n'
3899 'into .git/hooks/commit-msg and '
3900 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003901 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3902 except Exception:
3903 if os.path.exists(dst):
3904 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003905 DieWithError('\nFailed to download hooks.\n'
3906 'You need to download from\n%s\n'
3907 'into .git/hooks/commit-msg and '
3908 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003909
3910
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003911def GetRietveldCodereviewSettingsInteractively():
3912 """Prompt the user for settings."""
3913 server = settings.GetDefaultServerUrl(error_ok=True)
3914 prompt = 'Rietveld server (host[:port])'
3915 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3916 newserver = ask_for_data(prompt + ':')
3917 if not server and not newserver:
3918 newserver = DEFAULT_SERVER
3919 if newserver:
3920 newserver = gclient_utils.UpgradeToHttps(newserver)
3921 if newserver != server:
3922 RunGit(['config', 'rietveld.server', newserver])
3923
3924 def SetProperty(initial, caption, name, is_url):
3925 prompt = caption
3926 if initial:
3927 prompt += ' ("x" to clear) [%s]' % initial
3928 new_val = ask_for_data(prompt + ':')
3929 if new_val == 'x':
3930 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3931 elif new_val:
3932 if is_url:
3933 new_val = gclient_utils.UpgradeToHttps(new_val)
3934 if new_val != initial:
3935 RunGit(['config', 'rietveld.' + name, new_val])
3936
3937 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3938 SetProperty(settings.GetDefaultPrivateFlag(),
3939 'Private flag (rietveld only)', 'private', False)
3940 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3941 'tree-status-url', False)
3942 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3943 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3944 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3945 'run-post-upload-hook', False)
3946
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003947
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003948class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003949 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003950
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003951 _GOOGLESOURCE = 'googlesource.com'
3952
3953 def __init__(self):
3954 # Cached list of [host, identity, source], where source is either
3955 # .gitcookies or .netrc.
3956 self._all_hosts = None
3957
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003958 def ensure_configured_gitcookies(self):
3959 """Runs checks and suggests fixes to make git use .gitcookies from default
3960 path."""
3961 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3962 configured_path = RunGitSilent(
3963 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003964 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003965 if configured_path:
3966 self._ensure_default_gitcookies_path(configured_path, default)
3967 else:
3968 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003969
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003970 @staticmethod
3971 def _ensure_default_gitcookies_path(configured_path, default_path):
3972 assert configured_path
3973 if configured_path == default_path:
3974 print('git is already configured to use your .gitcookies from %s' %
3975 configured_path)
3976 return
3977
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003978 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003979 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3980 (configured_path, default_path))
3981
3982 if not os.path.exists(configured_path):
3983 print('However, your configured .gitcookies file is missing.')
3984 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3985 action='reconfigure')
3986 RunGit(['config', '--global', 'http.cookiefile', default_path])
3987 return
3988
3989 if os.path.exists(default_path):
3990 print('WARNING: default .gitcookies file already exists %s' %
3991 default_path)
3992 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3993 default_path)
3994
3995 confirm_or_exit('Move existing .gitcookies to default location?',
3996 action='move')
3997 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003998 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003999 print('Moved and reconfigured git to use .gitcookies from %s' %
4000 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004001
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004002 @staticmethod
4003 def _configure_gitcookies_path(default_path):
4004 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
4005 if os.path.exists(netrc_path):
4006 print('You seem to be using outdated .netrc for git credentials: %s' %
4007 netrc_path)
4008 print('This tool will guide you through setting up recommended '
4009 '.gitcookies store for git credentials.\n'
4010 '\n'
4011 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
4012 ' git config --global --unset http.cookiefile\n'
4013 ' mv %s %s.backup\n\n' % (default_path, default_path))
4014 confirm_or_exit(action='setup .gitcookies')
4015 RunGit(['config', '--global', 'http.cookiefile', default_path])
4016 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004017
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004018 def get_hosts_with_creds(self, include_netrc=False):
4019 if self._all_hosts is None:
4020 a = gerrit_util.CookiesAuthenticator()
4021 self._all_hosts = [
4022 (h, u, s)
4023 for h, u, s in itertools.chain(
4024 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
4025 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
4026 )
4027 if h.endswith(self._GOOGLESOURCE)
4028 ]
4029
4030 if include_netrc:
4031 return self._all_hosts
4032 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
4033
4034 def print_current_creds(self, include_netrc=False):
4035 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
4036 if not hosts:
4037 print('No Git/Gerrit credentials found')
4038 return
4039 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
4040 header = [('Host', 'User', 'Which file'),
4041 ['=' * l for l in lengths]]
4042 for row in (header + hosts):
4043 print('\t'.join((('%%+%ds' % l) % s)
4044 for l, s in zip(lengths, row)))
4045
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004046 @staticmethod
4047 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08004048 """Parses identity "git-<username>.domain" into <username> and domain."""
4049 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02004050 # distinguishable from sub-domains. But we do know typical domains:
4051 if identity.endswith('.chromium.org'):
4052 domain = 'chromium.org'
4053 username = identity[:-len('.chromium.org')]
4054 else:
4055 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004056 if username.startswith('git-'):
4057 username = username[len('git-'):]
4058 return username, domain
4059
4060 def _get_usernames_of_domain(self, domain):
4061 """Returns list of usernames referenced by .gitcookies in a given domain."""
4062 identities_by_domain = {}
4063 for _, identity, _ in self.get_hosts_with_creds():
4064 username, domain = self._parse_identity(identity)
4065 identities_by_domain.setdefault(domain, []).append(username)
4066 return identities_by_domain.get(domain)
4067
4068 def _canonical_git_googlesource_host(self, host):
4069 """Normalizes Gerrit hosts (with '-review') to Git host."""
4070 assert host.endswith(self._GOOGLESOURCE)
4071 # Prefix doesn't include '.' at the end.
4072 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
4073 if prefix.endswith('-review'):
4074 prefix = prefix[:-len('-review')]
4075 return prefix + '.' + self._GOOGLESOURCE
4076
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004077 def _canonical_gerrit_googlesource_host(self, host):
4078 git_host = self._canonical_git_googlesource_host(host)
4079 prefix = git_host.split('.', 1)[0]
4080 return prefix + '-review.' + self._GOOGLESOURCE
4081
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004082 def _get_counterpart_host(self, host):
4083 assert host.endswith(self._GOOGLESOURCE)
4084 git = self._canonical_git_googlesource_host(host)
4085 gerrit = self._canonical_gerrit_googlesource_host(git)
4086 return git if gerrit == host else gerrit
4087
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004088 def has_generic_host(self):
4089 """Returns whether generic .googlesource.com has been configured.
4090
4091 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
4092 """
4093 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
4094 if host == '.' + self._GOOGLESOURCE:
4095 return True
4096 return False
4097
4098 def _get_git_gerrit_identity_pairs(self):
4099 """Returns map from canonic host to pair of identities (Git, Gerrit).
4100
4101 One of identities might be None, meaning not configured.
4102 """
4103 host_to_identity_pairs = {}
4104 for host, identity, _ in self.get_hosts_with_creds():
4105 canonical = self._canonical_git_googlesource_host(host)
4106 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
4107 idx = 0 if canonical == host else 1
4108 pair[idx] = identity
4109 return host_to_identity_pairs
4110
4111 def get_partially_configured_hosts(self):
4112 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004113 (host if i1 else self._canonical_gerrit_googlesource_host(host))
4114 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
4115 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004116
4117 def get_conflicting_hosts(self):
4118 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004119 host
4120 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01004121 if None not in (i1, i2) and i1 != i2)
4122
4123 def get_duplicated_hosts(self):
4124 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
4125 return set(host for host, count in counters.iteritems() if count > 1)
4126
4127 _EXPECTED_HOST_IDENTITY_DOMAINS = {
4128 'chromium.googlesource.com': 'chromium.org',
4129 'chrome-internal.googlesource.com': 'google.com',
4130 }
4131
4132 def get_hosts_with_wrong_identities(self):
4133 """Finds hosts which **likely** reference wrong identities.
4134
4135 Note: skips hosts which have conflicting identities for Git and Gerrit.
4136 """
4137 hosts = set()
4138 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
4139 pair = self._get_git_gerrit_identity_pairs().get(host)
4140 if pair and pair[0] == pair[1]:
4141 _, domain = self._parse_identity(pair[0])
4142 if domain != expected:
4143 hosts.add(host)
4144 return hosts
4145
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004146 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004147 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004148 hosts = sorted(hosts)
4149 assert hosts
4150 if extra_column_func is None:
4151 extras = [''] * len(hosts)
4152 else:
4153 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004154 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
4155 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004156 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004157 lines.append(tmpl % he)
4158 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004159
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004160 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004161 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004162 yield ('.googlesource.com wildcard record detected',
4163 ['Chrome Infrastructure team recommends to list full host names '
4164 'explicitly.'],
4165 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004166
4167 dups = self.get_duplicated_hosts()
4168 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004169 yield ('The following hosts were defined twice',
4170 self._format_hosts(dups),
4171 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004172
4173 partial = self.get_partially_configured_hosts()
4174 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004175 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4176 'These hosts are missing',
4177 self._format_hosts(partial, lambda host: 'but %s defined' %
4178 self._get_counterpart_host(host)),
4179 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004180
4181 conflicting = self.get_conflicting_hosts()
4182 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004183 yield ('The following Git hosts have differing credentials from their '
4184 'Gerrit counterparts',
4185 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4186 tuple(self._get_git_gerrit_identity_pairs()[host])),
4187 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004188
4189 wrong = self.get_hosts_with_wrong_identities()
4190 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004191 yield ('These hosts likely use wrong identity',
4192 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4193 (self._get_git_gerrit_identity_pairs()[host][0],
4194 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4195 wrong)
4196
4197 def find_and_report_problems(self):
4198 """Returns True if there was at least one problem, else False."""
4199 found = False
4200 bad_hosts = set()
4201 for title, sublines, hosts in self._find_problems():
4202 if not found:
4203 found = True
4204 print('\n\n.gitcookies problem report:\n')
4205 bad_hosts.update(hosts or [])
4206 print(' %s%s' % (title , (':' if sublines else '')))
4207 if sublines:
4208 print()
4209 print(' %s' % '\n '.join(sublines))
4210 print()
4211
4212 if bad_hosts:
4213 assert found
4214 print(' You can manually remove corresponding lines in your %s file and '
4215 'visit the following URLs with correct account to generate '
4216 'correct credential lines:\n' %
4217 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4218 print(' %s' % '\n '.join(sorted(set(
4219 gerrit_util.CookiesAuthenticator().get_new_password_url(
4220 self._canonical_git_googlesource_host(host))
4221 for host in bad_hosts
4222 ))))
4223 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004224
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004225
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004226@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004227def CMDcreds_check(parser, args):
4228 """Checks credentials and suggests changes."""
4229 _, _ = parser.parse_args(args)
4230
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004231 # Code below checks .gitcookies. Abort if using something else.
4232 authn = gerrit_util.Authenticator.get()
4233 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
4234 if isinstance(authn, gerrit_util.GceAuthenticator):
4235 DieWithError(
4236 'This command is not designed for GCE, are you on a bot?\n'
4237 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
4238 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004239 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00004240 'This command is not designed for bot environment. It checks '
4241 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004242
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004243 checker = _GitCookiesChecker()
4244 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004245
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004246 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004247 checker.print_current_creds(include_netrc=True)
4248
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004249 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004250 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004251 return 0
4252 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004253
4254
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004255@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004256@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004258 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004259
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004260 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004261 # TODO(tandrii): remove this once we switch to Gerrit.
4262 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004263 parser.add_option('--activate-update', action='store_true',
4264 help='activate auto-updating [rietveld] section in '
4265 '.git/config')
4266 parser.add_option('--deactivate-update', action='store_true',
4267 help='deactivate auto-updating [rietveld] section in '
4268 '.git/config')
4269 options, args = parser.parse_args(args)
4270
4271 if options.deactivate_update:
4272 RunGit(['config', 'rietveld.autoupdate', 'false'])
4273 return
4274
4275 if options.activate_update:
4276 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4277 return
4278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004279 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004280 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281 return 0
4282
4283 url = args[0]
4284 if not url.endswith('codereview.settings'):
4285 url = os.path.join(url, 'codereview.settings')
4286
4287 # Load code review settings and download hooks (if available).
4288 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4289 return 0
4290
4291
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004292@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004293def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004294 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004295 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4296 branch = ShortBranchName(branchref)
4297 _, args = parser.parse_args(args)
4298 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004300 return RunGit(['config', 'branch.%s.base-url' % branch],
4301 error_ok=False).strip()
4302 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004303 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004304 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4305 error_ok=False).strip()
4306
4307
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004308def color_for_status(status):
4309 """Maps a Changelist status to color, for CMDstatus and other tools."""
4310 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004311 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004312 'waiting': Fore.BLUE,
4313 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004314 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004315 'lgtm': Fore.GREEN,
4316 'commit': Fore.MAGENTA,
4317 'closed': Fore.CYAN,
4318 'error': Fore.WHITE,
4319 }.get(status, Fore.WHITE)
4320
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004321
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004322def get_cl_statuses(changes, fine_grained, max_processes=None):
4323 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004324
4325 If fine_grained is true, this will fetch CL statuses from the server.
4326 Otherwise, simply indicate if there's a matching url for the given branches.
4327
4328 If max_processes is specified, it is used as the maximum number of processes
4329 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4330 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004331
4332 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004333 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004334 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004335 upload.verbosity = 0
4336
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004337 if not changes:
4338 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004339
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004340 if not fine_grained:
4341 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004342 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004343 for cl in changes:
4344 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004345 return
4346
4347 # First, sort out authentication issues.
4348 logging.debug('ensuring credentials exist')
4349 for cl in changes:
4350 cl.EnsureAuthenticated(force=False, refresh=True)
4351
4352 def fetch(cl):
4353 try:
4354 return (cl, cl.GetStatus())
4355 except:
4356 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004357 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004358 raise
4359
4360 threads_count = len(changes)
4361 if max_processes:
4362 threads_count = max(1, min(threads_count, max_processes))
4363 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4364
4365 pool = ThreadPool(threads_count)
4366 fetched_cls = set()
4367 try:
4368 it = pool.imap_unordered(fetch, changes).__iter__()
4369 while True:
4370 try:
4371 cl, status = it.next(timeout=5)
4372 except multiprocessing.TimeoutError:
4373 break
4374 fetched_cls.add(cl)
4375 yield cl, status
4376 finally:
4377 pool.close()
4378
4379 # Add any branches that failed to fetch.
4380 for cl in set(changes) - fetched_cls:
4381 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004382
rmistry@google.com2dd99862015-06-22 12:22:18 +00004383
4384def upload_branch_deps(cl, args):
4385 """Uploads CLs of local branches that are dependents of the current branch.
4386
4387 If the local branch dependency tree looks like:
4388 test1 -> test2.1 -> test3.1
4389 -> test3.2
4390 -> test2.2 -> test3.3
4391
4392 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4393 run on the dependent branches in this order:
4394 test2.1, test3.1, test3.2, test2.2, test3.3
4395
4396 Note: This function does not rebase your local dependent branches. Use it when
4397 you make a change to the parent branch that will not conflict with its
4398 dependent branches, and you would like their dependencies updated in
4399 Rietveld.
4400 """
4401 if git_common.is_dirty_git_tree('upload-branch-deps'):
4402 return 1
4403
4404 root_branch = cl.GetBranch()
4405 if root_branch is None:
4406 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4407 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004408 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004409 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4410 'patchset dependencies without an uploaded CL.')
4411
4412 branches = RunGit(['for-each-ref',
4413 '--format=%(refname:short) %(upstream:short)',
4414 'refs/heads'])
4415 if not branches:
4416 print('No local branches found.')
4417 return 0
4418
4419 # Create a dictionary of all local branches to the branches that are dependent
4420 # on it.
4421 tracked_to_dependents = collections.defaultdict(list)
4422 for b in branches.splitlines():
4423 tokens = b.split()
4424 if len(tokens) == 2:
4425 branch_name, tracked = tokens
4426 tracked_to_dependents[tracked].append(branch_name)
4427
vapiera7fbd5a2016-06-16 09:17:49 -07004428 print()
4429 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004430 dependents = []
4431 def traverse_dependents_preorder(branch, padding=''):
4432 dependents_to_process = tracked_to_dependents.get(branch, [])
4433 padding += ' '
4434 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004435 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004436 dependents.append(dependent)
4437 traverse_dependents_preorder(dependent, padding)
4438 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004439 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004440
4441 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004442 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004443 return 0
4444
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004445 confirm_or_exit('This command will checkout all dependent branches and run '
4446 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004447
andybons@chromium.org962f9462016-02-03 20:00:42 +00004448 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004449 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004450 args.extend(['-t', 'Updated patchset dependency'])
4451
rmistry@google.com2dd99862015-06-22 12:22:18 +00004452 # Record all dependents that failed to upload.
4453 failures = {}
4454 # Go through all dependents, checkout the branch and upload.
4455 try:
4456 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004457 print()
4458 print('--------------------------------------')
4459 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004460 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004462 try:
4463 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004464 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004465 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004466 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004467 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004468 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004469 finally:
4470 # Swap back to the original root branch.
4471 RunGit(['checkout', '-q', root_branch])
4472
vapiera7fbd5a2016-06-16 09:17:49 -07004473 print()
4474 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004475 for dependent_branch in dependents:
4476 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004477 print(' %s : %s' % (dependent_branch, upload_status))
4478 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004479
4480 return 0
4481
4482
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004483@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004484def CMDarchive(parser, args):
4485 """Archives and deletes branches associated with closed changelists."""
4486 parser.add_option(
4487 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004488 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004489 parser.add_option(
4490 '-f', '--force', action='store_true',
4491 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004492 parser.add_option(
4493 '-d', '--dry-run', action='store_true',
4494 help='Skip the branch tagging and removal steps.')
4495 parser.add_option(
4496 '-t', '--notags', action='store_true',
4497 help='Do not tag archived branches. '
4498 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004499
4500 auth.add_auth_options(parser)
4501 options, args = parser.parse_args(args)
4502 if args:
4503 parser.error('Unsupported args: %s' % ' '.join(args))
4504 auth_config = auth.extract_auth_config_from_options(options)
4505
4506 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4507 if not branches:
4508 return 0
4509
vapiera7fbd5a2016-06-16 09:17:49 -07004510 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004511 changes = [Changelist(branchref=b, auth_config=auth_config)
4512 for b in branches.splitlines()]
4513 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4514 statuses = get_cl_statuses(changes,
4515 fine_grained=True,
4516 max_processes=options.maxjobs)
4517 proposal = [(cl.GetBranch(),
4518 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4519 for cl, status in statuses
4520 if status == 'closed']
4521 proposal.sort()
4522
4523 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004524 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004525 return 0
4526
4527 current_branch = GetCurrentBranch()
4528
vapiera7fbd5a2016-06-16 09:17:49 -07004529 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004530 if options.notags:
4531 for next_item in proposal:
4532 print(' ' + next_item[0])
4533 else:
4534 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4535 for next_item in proposal:
4536 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004537
kmarshall9249e012016-08-23 12:02:16 -07004538 # Quit now on precondition failure or if instructed by the user, either
4539 # via an interactive prompt or by command line flags.
4540 if options.dry_run:
4541 print('\nNo changes were made (dry run).\n')
4542 return 0
4543 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004544 print('You are currently on a branch \'%s\' which is associated with a '
4545 'closed codereview issue, so archive cannot proceed. Please '
4546 'checkout another branch and run this command again.' %
4547 current_branch)
4548 return 1
kmarshall9249e012016-08-23 12:02:16 -07004549 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004550 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4551 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004552 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004553 return 1
4554
4555 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004556 if not options.notags:
4557 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004558 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004559
vapiera7fbd5a2016-06-16 09:17:49 -07004560 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004561
4562 return 0
4563
4564
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004565@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004566def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004567 """Show status of changelists.
4568
4569 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004570 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004571 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004572 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004573 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004574 - Magenta in the commit queue
4575 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004576 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004577
4578 Also see 'git cl comments'.
4579 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004580 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004581 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004582 parser.add_option('-f', '--fast', action='store_true',
4583 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004584 parser.add_option(
4585 '-j', '--maxjobs', action='store', type=int,
4586 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004587
4588 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004589 _add_codereview_issue_select_options(
4590 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004591 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004592 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004593 if args:
4594 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004595 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004596
iannuccie53c9352016-08-17 14:40:40 -07004597 if options.issue is not None and not options.field:
4598 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004600 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004601 cl = Changelist(auth_config=auth_config, issue=options.issue,
4602 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004603 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004604 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004605 elif options.field == 'id':
4606 issueid = cl.GetIssue()
4607 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004608 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004609 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004610 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004612 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004613 elif options.field == 'status':
4614 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004615 elif options.field == 'url':
4616 url = cl.GetIssueURL()
4617 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004618 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004619 return 0
4620
4621 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4622 if not branches:
4623 print('No local branch found.')
4624 return 0
4625
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004626 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004627 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004628 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004629 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004630 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004631 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004632 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004633
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004634 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004635 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4636 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4637 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004638 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004639 c, status = output.next()
4640 branch_statuses[c.GetBranch()] = status
4641 status = branch_statuses.pop(branch)
4642 url = cl.GetIssueURL()
4643 if url and (not status or status == 'error'):
4644 # The issue probably doesn't exist anymore.
4645 url += ' (broken)'
4646
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004647 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004648 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004649 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004650 color = ''
4651 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004652 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004653 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004654 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004655 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004656
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004657
4658 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004659 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004660 print('Current branch: %s' % branch)
4661 for cl in changes:
4662 if cl.GetBranch() == branch:
4663 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004664 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004665 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004666 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004667 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004668 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004669 print('Issue description:')
4670 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004671 return 0
4672
4673
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004674def colorize_CMDstatus_doc():
4675 """To be called once in main() to add colors to git cl status help."""
4676 colors = [i for i in dir(Fore) if i[0].isupper()]
4677
4678 def colorize_line(line):
4679 for color in colors:
4680 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004681 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004682 indent = len(line) - len(line.lstrip(' ')) + 1
4683 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4684 return line
4685
4686 lines = CMDstatus.__doc__.splitlines()
4687 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4688
4689
phajdan.jre328cf92016-08-22 04:12:17 -07004690def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004691 if path == '-':
4692 json.dump(contents, sys.stdout)
4693 else:
4694 with open(path, 'w') as f:
4695 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004696
4697
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004698@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004699@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004700def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004701 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004702
4703 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004704 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004705 parser.add_option('-r', '--reverse', action='store_true',
4706 help='Lookup the branch(es) for the specified issues. If '
4707 'no issues are specified, all branches with mapped '
4708 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004709 parser.add_option('--json',
4710 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004711 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004712 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004713 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004714
dnj@chromium.org406c4402015-03-03 17:22:28 +00004715 if options.reverse:
4716 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004717 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004718 # Reverse issue lookup.
4719 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004720
4721 git_config = {}
4722 for config in RunGit(['config', '--get-regexp',
4723 r'branch\..*issue']).splitlines():
4724 name, _space, val = config.partition(' ')
4725 git_config[name] = val
4726
dnj@chromium.org406c4402015-03-03 17:22:28 +00004727 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004728 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4729 config_key = _git_branch_config_key(ShortBranchName(branch),
4730 cls.IssueConfigKey())
4731 issue = git_config.get(config_key)
4732 if issue:
4733 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004734 if not args:
4735 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004736 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004737 for issue in args:
4738 if not issue:
4739 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004740 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004741 print('Branch for issue number %s: %s' % (
4742 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004743 if options.json:
4744 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004745 return 0
4746
4747 if len(args) > 0:
4748 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4749 if not issue.valid:
4750 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4751 'or no argument to list it.\n'
4752 'Maybe you want to run git cl status?')
4753 cl = Changelist(codereview=issue.codereview)
4754 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004755 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004756 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004757 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4758 if options.json:
4759 write_json(options.json, {
4760 'issue': cl.GetIssue(),
4761 'issue_url': cl.GetIssueURL(),
4762 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004763 return 0
4764
4765
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004766@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004767def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004768 """Shows or posts review comments for any changelist."""
4769 parser.add_option('-a', '--add-comment', dest='comment',
4770 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004771 parser.add_option('-i', '--issue', dest='issue',
4772 help='review issue id (defaults to current issue). '
4773 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004774 parser.add_option('-m', '--machine-readable', dest='readable',
4775 action='store_false', default=True,
4776 help='output comments in a format compatible with '
4777 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004778 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004779 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004780 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004781 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004782 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004783 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004784 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004785
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004786 issue = None
4787 if options.issue:
4788 try:
4789 issue = int(options.issue)
4790 except ValueError:
4791 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004792 if not options.forced_codereview:
4793 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004794
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004795 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004796 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004797 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004798
4799 if options.comment:
4800 cl.AddComment(options.comment)
4801 return 0
4802
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004803 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4804 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004805 for comment in summary:
4806 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004807 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004808 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004809 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004810 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004811 color = Fore.MAGENTA
4812 else:
4813 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004814 print('\n%s%s %s%s\n%s' % (
4815 color,
4816 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4817 comment.sender,
4818 Fore.RESET,
4819 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4820
smut@google.comc85ac942015-09-15 16:34:43 +00004821 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004822 def pre_serialize(c):
4823 dct = c.__dict__.copy()
4824 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4825 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004826 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004827 return 0
4828
4829
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004830@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004831@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004832def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004833 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004834 parser.add_option('-d', '--display', action='store_true',
4835 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004836 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004837 help='New description to set for this issue (- for stdin, '
4838 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004839 parser.add_option('-f', '--force', action='store_true',
4840 help='Delete any unpublished Gerrit edits for this issue '
4841 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004842
4843 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004844 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004845 options, args = parser.parse_args(args)
4846 _process_codereview_select_options(parser, options)
4847
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004848 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004849 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004850 target_issue_arg = ParseIssueNumberArgument(args[0],
4851 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004852 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004853 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004854
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004855 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004856
martiniss6eda05f2016-06-30 10:18:35 -07004857 kwargs = {
4858 'auth_config': auth_config,
4859 'codereview': options.forced_codereview,
4860 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004861 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004862 if target_issue_arg:
4863 kwargs['issue'] = target_issue_arg.issue
4864 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004865 if target_issue_arg.codereview and not options.forced_codereview:
4866 detected_codereview_from_url = True
4867 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004868
4869 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004870 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004871 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004872 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004873
4874 if detected_codereview_from_url:
4875 logging.info('canonical issue/change URL: %s (type: %s)\n',
4876 cl.GetIssueURL(), target_issue_arg.codereview)
4877
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004878 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004879
smut@google.com34fb6b12015-07-13 20:03:26 +00004880 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004881 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004882 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004883
4884 if options.new_description:
4885 text = options.new_description
4886 if text == '-':
4887 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004888 elif text == '+':
4889 base_branch = cl.GetCommonAncestorWithUpstream()
4890 change = cl.GetChange(base_branch, None, local_description=True)
4891 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004892
4893 description.set_description(text)
4894 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004895 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004896
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004897 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004898 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004899 return 0
4900
4901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004902def CreateDescriptionFromLog(args):
4903 """Pulls out the commit log to use as a base for the CL description."""
4904 log_args = []
4905 if len(args) == 1 and not args[0].endswith('.'):
4906 log_args = [args[0] + '..']
4907 elif len(args) == 1 and args[0].endswith('...'):
4908 log_args = [args[0][:-1]]
4909 elif len(args) == 2:
4910 log_args = [args[0] + '..' + args[1]]
4911 else:
4912 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004913 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004914
4915
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004916@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004917def CMDlint(parser, args):
4918 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004919 parser.add_option('--filter', action='append', metavar='-x,+y',
4920 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004921 auth.add_auth_options(parser)
4922 options, args = parser.parse_args(args)
4923 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004924
4925 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004926 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004927 try:
4928 import cpplint
4929 import cpplint_chromium
4930 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004931 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004932 return 1
4933
4934 # Change the current working directory before calling lint so that it
4935 # shows the correct base.
4936 previous_cwd = os.getcwd()
4937 os.chdir(settings.GetRoot())
4938 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004939 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004940 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4941 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004942 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004943 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004944 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004945
4946 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004947 command = args + files
4948 if options.filter:
4949 command = ['--filter=' + ','.join(options.filter)] + command
4950 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004951
4952 white_regex = re.compile(settings.GetLintRegex())
4953 black_regex = re.compile(settings.GetLintIgnoreRegex())
4954 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4955 for filename in filenames:
4956 if white_regex.match(filename):
4957 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004958 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004959 else:
4960 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4961 extra_check_functions)
4962 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004963 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004964 finally:
4965 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004966 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004967 if cpplint._cpplint_state.error_count != 0:
4968 return 1
4969 return 0
4970
4971
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004972@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004973def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004974 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004975 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004976 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004977 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004978 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004979 parser.add_option('--all', action='store_true',
4980 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004981 parser.add_option('--parallel', action='store_true',
4982 help='Run all tests specified by input_api.RunTests in all '
4983 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004984 auth.add_auth_options(parser)
4985 options, args = parser.parse_args(args)
4986 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004987
sbc@chromium.org71437c02015-04-09 19:29:40 +00004988 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004989 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004990 return 1
4991
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004992 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004993 if args:
4994 base_branch = args[0]
4995 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004996 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004997 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004998
Aaron Gable8076c282017-11-29 14:39:41 -08004999 if options.all:
5000 base_change = cl.GetChange(base_branch, None)
5001 files = [('M', f) for f in base_change.AllFiles()]
5002 change = presubmit_support.GitChange(
5003 base_change.Name(),
5004 base_change.FullDescriptionText(),
5005 base_change.RepositoryRoot(),
5006 files,
5007 base_change.issue,
5008 base_change.patchset,
5009 base_change.author_email,
5010 base_change._upstream)
5011 else:
5012 change = cl.GetChange(base_branch, None)
5013
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00005014 cl.RunHook(
5015 committing=not options.upload,
5016 may_prompt=False,
5017 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04005018 change=change,
5019 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00005020 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005021
5022
tandrii@chromium.org65874e12016-03-04 12:03:02 +00005023def GenerateGerritChangeId(message):
5024 """Returns Ixxxxxx...xxx change id.
5025
5026 Works the same way as
5027 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
5028 but can be called on demand on all platforms.
5029
5030 The basic idea is to generate git hash of a state of the tree, original commit
5031 message, author/committer info and timestamps.
5032 """
5033 lines = []
5034 tree_hash = RunGitSilent(['write-tree'])
5035 lines.append('tree %s' % tree_hash.strip())
5036 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
5037 if code == 0:
5038 lines.append('parent %s' % parent.strip())
5039 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
5040 lines.append('author %s' % author.strip())
5041 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
5042 lines.append('committer %s' % committer.strip())
5043 lines.append('')
5044 # Note: Gerrit's commit-hook actually cleans message of some lines and
5045 # whitespace. This code is not doing this, but it clearly won't decrease
5046 # entropy.
5047 lines.append(message)
5048 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
5049 stdin='\n'.join(lines))
5050 return 'I%s' % change_hash.strip()
5051
5052
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005053def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00005054 """Computes the remote branch ref to use for the CL.
5055
5056 Args:
5057 remote (str): The git remote for the CL.
5058 remote_branch (str): The git remote branch for the CL.
5059 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00005060 """
5061 if not (remote and remote_branch):
5062 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005063
wittman@chromium.org455dc922015-01-26 20:15:50 +00005064 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005065 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00005066 # refs, which are then translated into the remote full symbolic refs
5067 # below.
5068 if '/' not in target_branch:
5069 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
5070 else:
5071 prefix_replacements = (
5072 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
5073 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
5074 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
5075 )
5076 match = None
5077 for regex, replacement in prefix_replacements:
5078 match = re.search(regex, target_branch)
5079 if match:
5080 remote_branch = target_branch.replace(match.group(0), replacement)
5081 break
5082 if not match:
5083 # This is a branch path but not one we recognize; use as-is.
5084 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00005085 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
5086 # Handle the refs that need to land in different refs.
5087 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005088
wittman@chromium.org455dc922015-01-26 20:15:50 +00005089 # Create the true path to the remote branch.
5090 # Does the following translation:
5091 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
5092 # * refs/remotes/origin/master -> refs/heads/master
5093 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
5094 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
5095 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
5096 elif remote_branch.startswith('refs/remotes/%s/' % remote):
5097 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
5098 'refs/heads/')
5099 elif remote_branch.startswith('refs/remotes/branch-heads'):
5100 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01005101
wittman@chromium.org455dc922015-01-26 20:15:50 +00005102 return remote_branch
5103
5104
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005105def cleanup_list(l):
5106 """Fixes a list so that comma separated items are put as individual items.
5107
5108 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
5109 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
5110 """
5111 items = sum((i.split(',') for i in l), [])
5112 stripped_items = (i.strip() for i in items)
5113 return sorted(filter(None, stripped_items))
5114
5115
Aaron Gable4db38df2017-11-03 14:59:07 -07005116@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005117@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005118def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00005119 """Uploads the current changelist to codereview.
5120
5121 Can skip dependency patchset uploads for a branch by running:
5122 git config branch.branch_name.skip-deps-uploads True
5123 To unset run:
5124 git config --unset branch.branch_name.skip-deps-uploads
5125 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02005126
5127 If the name of the checked out branch starts with "bug-" or "fix-" followed by
5128 a bug number, this bug number is automatically populated in the CL
5129 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005130
5131 If subject contains text in square brackets or has "<text>: " prefix, such
5132 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
5133 [git-cl] add support for hashtags
5134 Foo bar: implement foo
5135 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00005136 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00005137 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5138 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00005139 parser.add_option('--bypass-watchlists', action='store_true',
5140 dest='bypass_watchlists',
5141 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005142 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00005143 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005144 parser.add_option('--message', '-m', dest='message',
5145 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07005146 parser.add_option('-b', '--bug',
5147 help='pre-populate the bug number(s) for this issue. '
5148 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07005149 parser.add_option('--message-file', dest='message_file',
5150 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005151 parser.add_option('--title', '-t', dest='title',
5152 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005153 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005154 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005155 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005156 parser.add_option('--tbrs',
5157 action='append', default=[],
5158 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00005159 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005160 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00005161 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005162 parser.add_option('--hashtag', dest='hashtags',
5163 action='append', default=[],
5164 help=('Gerrit hashtag for new CL; '
5165 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00005166 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08005167 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005168 parser.add_option('--emulate_svn_auto_props',
5169 '--emulate-svn-auto-props',
5170 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00005171 dest="emulate_svn_auto_props",
5172 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00005173 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07005174 help='tell the commit queue to commit this patchset; '
5175 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00005176 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00005177 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00005178 metavar='TARGET',
5179 help='Apply CL to remote ref TARGET. ' +
5180 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00005181 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005182 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00005183 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005184 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07005185 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005186 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07005187 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5188 const='TBR', help='add a set of OWNERS to TBR')
5189 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5190 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005191 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5192 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005193 help='Send the patchset to do a CQ dry run right after '
5194 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005195 parser.add_option('--dependencies', action='store_true',
5196 help='Uploads CLs of all the local branches that depend on '
5197 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005198 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5199 help='Sends your change to the CQ after an approval. Only '
5200 'works on repos that have the Auto-Submit label '
5201 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04005202 parser.add_option('--parallel', action='store_true',
5203 help='Run all tests specified by input_api.RunTests in all '
5204 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005205
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005206 parser.add_option('--no-autocc', action='store_true',
5207 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005208 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005209 help='Set the review private. This implies --no-autocc.')
5210
5211 # TODO: remove Rietveld flags
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005212 parser.add_option('--email', default=None,
5213 help='email address to use to connect to Rietveld')
5214
rmistry@google.com2dd99862015-06-22 12:22:18 +00005215 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005216 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005217 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005218 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005219 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005220 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005221
sbc@chromium.org71437c02015-04-09 19:29:40 +00005222 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005223 return 1
5224
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005225 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005226 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005227 options.cc = cleanup_list(options.cc)
5228
tandriib80458a2016-06-23 12:20:07 -07005229 if options.message_file:
5230 if options.message:
5231 parser.error('only one of --message and --message-file allowed.')
5232 options.message = gclient_utils.FileRead(options.message_file)
5233 options.message_file = None
5234
tandrii4d0545a2016-07-06 03:56:49 -07005235 if options.cq_dry_run and options.use_commit_queue:
5236 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5237
Aaron Gableedbc4132017-09-11 13:22:28 -07005238 if options.use_commit_queue:
5239 options.send_mail = True
5240
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005241 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5242 settings.GetIsGerrit()
5243
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005244 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005245 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005246
5247
Francois Dorayd42c6812017-05-30 15:10:20 -04005248@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005249@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005250def CMDsplit(parser, args):
5251 """Splits a branch into smaller branches and uploads CLs.
5252
5253 Creates a branch and uploads a CL for each group of files modified in the
5254 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005255 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005256 the shared OWNERS file.
5257 """
5258 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005259 help="A text file containing a CL description in which "
5260 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005261 parser.add_option("-c", "--comment", dest="comment_file",
5262 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005263 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5264 default=False,
5265 help="List the files and reviewers for each CL that would "
5266 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00005267 parser.add_option("--cq-dry-run", action='store_true',
5268 help="If set, will do a cq dry run for each uploaded CL. "
5269 "Please be careful when doing this; more than ~10 CLs "
5270 "has the potential to overload our build "
5271 "infrastructure. Try to upload these not during high "
5272 "load times (usually 11-3 Mountain View time). Email "
5273 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005274 options, _ = parser.parse_args(args)
5275
5276 if not options.description_file:
5277 parser.error('No --description flag specified.')
5278
5279 def WrappedCMDupload(args):
5280 return CMDupload(OptionParser(), args)
5281
5282 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00005283 Changelist, WrappedCMDupload, options.dry_run,
5284 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005285
5286
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005287@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005288@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005289def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005290 """DEPRECATED: Used to commit the current changelist via git-svn."""
5291 message = ('git-cl no longer supports committing to SVN repositories via '
5292 'git-svn. You probably want to use `git cl land` instead.')
5293 print(message)
5294 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005295
5296
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005297# Two special branches used by git cl land.
5298MERGE_BRANCH = 'git-cl-commit'
5299CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5300
5301
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005302@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005303@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005304def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005305 """Commits the current changelist via git.
5306
5307 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5308 upstream and closes the issue automatically and atomically.
5309
5310 Otherwise (in case of Rietveld):
5311 Squashes branch into a single commit.
5312 Updates commit message with metadata (e.g. pointer to review).
5313 Pushes the code upstream.
5314 Updates review and closes.
5315 """
5316 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5317 help='bypass upload presubmit hook')
5318 parser.add_option('-m', dest='message',
5319 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005320 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005321 help="force yes to questions (don't prompt)")
5322 parser.add_option('-c', dest='contributor',
5323 help="external contributor for patch (appended to " +
5324 "description and used as author for git). Should be " +
5325 "formatted as 'First Last <email@example.com>'")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005326 parser.add_option('--parallel', action='store_true',
5327 help='Run all tests specified by input_api.RunTests in all '
5328 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005329 auth.add_auth_options(parser)
5330 (options, args) = parser.parse_args(args)
5331 auth_config = auth.extract_auth_config_from_options(options)
5332
5333 cl = Changelist(auth_config=auth_config)
5334
Robert Iannucci2e73d432018-03-14 01:10:47 -07005335 if not cl.IsGerrit():
5336 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005337
Robert Iannucci2e73d432018-03-14 01:10:47 -07005338 if options.message:
5339 # This could be implemented, but it requires sending a new patch to
5340 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5341 # Besides, Gerrit has the ability to change the commit message on submit
5342 # automatically, thus there is no need to support this option (so far?).
5343 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005344 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005345 parser.error(
5346 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5347 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5348 'the contributor\'s "name <email>". If you can\'t upload such a '
5349 'commit for review, contact your repository admin and request'
5350 '"Forge-Author" permission.')
5351 if not cl.GetIssue():
5352 DieWithError('You must upload the change first to Gerrit.\n'
5353 ' If you would rather have `git cl land` upload '
5354 'automatically for you, see http://crbug.com/642759')
5355 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005356 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005357
5358
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005359def PushToGitWithAutoRebase(remote, branch, original_description,
5360 git_numberer_enabled, max_attempts=3):
5361 """Pushes current HEAD commit on top of remote's branch.
5362
5363 Attempts to fetch and autorebase on push failures.
5364 Adds git number footers on the fly.
5365
5366 Returns integer code from last command.
5367 """
5368 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5369 code = 0
5370 attempts_left = max_attempts
5371 while attempts_left:
5372 attempts_left -= 1
5373 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5374
5375 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5376 # If fetch fails, retry.
5377 print('Fetching %s/%s...' % (remote, branch))
5378 code, out = RunGitWithCode(
5379 ['retry', 'fetch', remote,
5380 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5381 if code:
5382 print('Fetch failed with exit code %d.' % code)
5383 print(out.strip())
5384 continue
5385
5386 print('Cherry-picking commit on top of latest %s' % branch)
5387 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5388 suppress_stderr=True)
5389 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5390 code, out = RunGitWithCode(['cherry-pick', cherry])
5391 if code:
5392 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5393 'the following files have merge conflicts:' %
5394 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005395 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5396 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005397 print('Please rebase your patch and try again.')
5398 RunGitWithCode(['cherry-pick', '--abort'])
5399 break
5400
5401 commit_desc = ChangeDescription(original_description)
5402 if git_numberer_enabled:
5403 logging.debug('Adding git number footers')
5404 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5405 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5406 branch)
5407 # Ensure timestamps are monotonically increasing.
5408 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5409 _get_committer_timestamp('HEAD'))
5410 _git_amend_head(commit_desc.description, timestamp)
5411
5412 code, out = RunGitWithCode(
5413 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5414 print(out)
5415 if code == 0:
5416 break
5417 if IsFatalPushFailure(out):
5418 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005419 'user.email are correct and you have push access to the repo.\n'
5420 'Hint: run command below to diangose common Git/Gerrit credential '
5421 'problems:\n'
5422 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005423 break
5424 return code
5425
5426
5427def IsFatalPushFailure(push_stdout):
5428 """True if retrying push won't help."""
5429 return '(prohibited by Gerrit)' in push_stdout
5430
5431
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005432@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005433@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005434def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005435 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005436 parser.add_option('-b', dest='newbranch',
5437 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005438 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005439 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005440 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005441 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005442 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005443 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005444 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005445 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005446 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005447 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005448
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005449
5450 group = optparse.OptionGroup(
5451 parser,
5452 'Options for continuing work on the current issue uploaded from a '
5453 'different clone (e.g. different machine). Must be used independently '
5454 'from the other options. No issue number should be specified, and the '
5455 'branch must have an issue number associated with it')
5456 group.add_option('--reapply', action='store_true', dest='reapply',
5457 help='Reset the branch and reapply the issue.\n'
5458 'CAUTION: This will undo any local changes in this '
5459 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005460
5461 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005462 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005463 parser.add_option_group(group)
5464
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005465 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005466 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005467 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005468 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005469 auth_config = auth.extract_auth_config_from_options(options)
5470
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005471 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005472 if options.newbranch:
5473 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005474 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005475 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005476
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005477 cl = Changelist(auth_config=auth_config,
5478 codereview=options.forced_codereview)
5479 if not cl.GetIssue():
5480 parser.error('current branch must have an associated issue')
5481
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005482 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005483 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005484 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005485
5486 RunGit(['reset', '--hard', upstream])
5487 if options.pull:
5488 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005489
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005490 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5491 options.directory)
5492
5493 if len(args) != 1 or not args[0]:
5494 parser.error('Must specify issue number or url')
5495
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005496 target_issue_arg = ParseIssueNumberArgument(args[0],
5497 options.forced_codereview)
5498 if not target_issue_arg.valid:
5499 parser.error('invalid codereview url or CL id')
5500
5501 cl_kwargs = {
5502 'auth_config': auth_config,
5503 'codereview_host': target_issue_arg.hostname,
5504 'codereview': options.forced_codereview,
5505 }
5506 detected_codereview_from_url = False
5507 if target_issue_arg.codereview and not options.forced_codereview:
5508 detected_codereview_from_url = True
5509 cl_kwargs['codereview'] = target_issue_arg.codereview
5510 cl_kwargs['issue'] = target_issue_arg.issue
5511
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005512 # We don't want uncommitted changes mixed up with the patch.
5513 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005514 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005515
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005516 if options.newbranch:
5517 if options.force:
5518 RunGit(['branch', '-D', options.newbranch],
5519 stderr=subprocess2.PIPE, error_ok=True)
5520 RunGit(['new-branch', options.newbranch])
5521
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005522 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005523
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005524 if cl.IsGerrit():
5525 if options.reject:
5526 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005527 if options.directory:
5528 parser.error('--directory is not supported with Gerrit codereview.')
5529
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005530 if detected_codereview_from_url:
5531 print('canonical issue/change URL: %s (type: %s)\n' %
5532 (cl.GetIssueURL(), target_issue_arg.codereview))
5533
5534 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005535 options.nocommit, options.directory,
5536 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005537
5538
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005539def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005540 """Fetches the tree status and returns either 'open', 'closed',
5541 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005542 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005543 if url:
5544 status = urllib2.urlopen(url).read().lower()
5545 if status.find('closed') != -1 or status == '0':
5546 return 'closed'
5547 elif status.find('open') != -1 or status == '1':
5548 return 'open'
5549 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005550 return 'unset'
5551
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005552
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005553def GetTreeStatusReason():
5554 """Fetches the tree status from a json url and returns the message
5555 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005556 url = settings.GetTreeStatusUrl()
5557 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005558 connection = urllib2.urlopen(json_url)
5559 status = json.loads(connection.read())
5560 connection.close()
5561 return status['message']
5562
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005563
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005564@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005565def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005566 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005567 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005568 status = GetTreeStatus()
5569 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005570 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005571 return 2
5572
vapiera7fbd5a2016-06-16 09:17:49 -07005573 print('The tree is %s' % status)
5574 print()
5575 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005576 if status != 'open':
5577 return 1
5578 return 0
5579
5580
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005581@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005582def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005583 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005584 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005585 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005586 '-b', '--bot', action='append',
5587 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5588 'times to specify multiple builders. ex: '
5589 '"-b win_rel -b win_layout". See '
5590 'the try server waterfall for the builders name and the tests '
5591 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005592 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005593 '-B', '--bucket', default='',
5594 help=('Buildbucket bucket to send the try requests.'))
5595 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005596 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005597 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005598 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005599 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005600 help='Revision to use for the try job; default: the revision will '
5601 'be determined by the try recipe that builder runs, which usually '
5602 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005603 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005604 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005605 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005606 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005607 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005608 '--category', default='git_cl_try', help='Specify custom build category.')
5609 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005610 '--project',
5611 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005612 'in recipe to determine to which repository or directory to '
5613 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005614 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005615 '-p', '--property', dest='properties', action='append', default=[],
5616 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005617 'key2=value2 etc. The value will be treated as '
5618 'json if decodable, or as string otherwise. '
5619 'NOTE: using this may make your try job not usable for CQ, '
5620 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005621 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005622 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5623 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005624 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005625 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005626 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005627 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005628 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005629 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005630
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005631 if options.master and options.master.startswith('luci.'):
5632 parser.error(
5633 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005634 # Make sure that all properties are prop=value pairs.
5635 bad_params = [x for x in options.properties if '=' not in x]
5636 if bad_params:
5637 parser.error('Got properties with missing "=": %s' % bad_params)
5638
maruel@chromium.org15192402012-09-06 12:38:29 +00005639 if args:
5640 parser.error('Unknown arguments: %s' % args)
5641
Koji Ishii31c14782018-01-08 17:17:33 +09005642 cl = Changelist(auth_config=auth_config, issue=options.issue,
5643 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005644 if not cl.GetIssue():
5645 parser.error('Need to upload first')
5646
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005647 if cl.IsGerrit():
5648 # HACK: warm up Gerrit change detail cache to save on RPCs.
5649 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5650
tandriie113dfd2016-10-11 10:20:12 -07005651 error_message = cl.CannotTriggerTryJobReason()
5652 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005653 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005654
borenet6c0efe62016-10-19 08:13:29 -07005655 if options.bucket and options.master:
5656 parser.error('Only one of --bucket and --master may be used.')
5657
qyearsley1fdfcb62016-10-24 13:22:03 -07005658 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005659
qyearsleydd49f942016-10-28 11:57:22 -07005660 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5661 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005662 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005663 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005664 print('git cl try with no bots now defaults to CQ dry run.')
5665 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5666 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005667
borenet6c0efe62016-10-19 08:13:29 -07005668 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005669 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005670 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005671 'of bot requires an initial job from a parent (usually a builder). '
5672 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005673 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005674 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005675
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005676 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005677 # TODO(tandrii): Checking local patchset against remote patchset is only
5678 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5679 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005680 print('Warning: Codereview server has newer patchsets (%s) than most '
5681 'recent upload from local checkout (%s). Did a previous upload '
5682 'fail?\n'
5683 'By default, git cl try uses the latest patchset from '
5684 'codereview, continuing to use patchset %s.\n' %
5685 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005686
tandrii568043b2016-10-11 07:49:18 -07005687 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005688 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005689 except BuildbucketResponseException as ex:
5690 print('ERROR: %s' % ex)
5691 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005692 return 0
5693
5694
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005695@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005696def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005697 """Prints info about try jobs associated with current CL."""
5698 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005699 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005700 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005701 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005702 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005703 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005704 '--color', action='store_true', default=setup_color.IS_TTY,
5705 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005706 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005707 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5708 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005709 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005710 '--json', help=('Path of JSON output file to write try job results to,'
5711 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005712 parser.add_option_group(group)
5713 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005714 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005715 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005716 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005717 if args:
5718 parser.error('Unrecognized args: %s' % ' '.join(args))
5719
5720 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005721 cl = Changelist(
5722 issue=options.issue, codereview=options.forced_codereview,
5723 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005724 if not cl.GetIssue():
5725 parser.error('Need to upload first')
5726
tandrii221ab252016-10-06 08:12:04 -07005727 patchset = options.patchset
5728 if not patchset:
5729 patchset = cl.GetMostRecentPatchset()
5730 if not patchset:
5731 parser.error('Codereview doesn\'t know about issue %s. '
5732 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005733 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005734 cl.GetIssue())
5735
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005736 # TODO(tandrii): Checking local patchset against remote patchset is only
5737 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5738 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005739 print('Warning: Codereview server has newer patchsets (%s) than most '
5740 'recent upload from local checkout (%s). Did a previous upload '
5741 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005742 'By default, git cl try-results uses the latest patchset from '
5743 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005744 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005745 try:
tandrii221ab252016-10-06 08:12:04 -07005746 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005747 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005748 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005749 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005750 if options.json:
5751 write_try_results_json(options.json, jobs)
5752 else:
5753 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005754 return 0
5755
5756
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005757@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005758@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005759def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005760 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005761 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005762 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005763 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005765 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005766 if args:
5767 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005768 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005769 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005770 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005771 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005772
5773 # Clear configured merge-base, if there is one.
5774 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005775 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005776 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005777 return 0
5778
5779
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005780@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005781def CMDweb(parser, args):
5782 """Opens the current CL in the web browser."""
5783 _, args = parser.parse_args(args)
5784 if args:
5785 parser.error('Unrecognized args: %s' % ' '.join(args))
5786
5787 issue_url = Changelist().GetIssueURL()
5788 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005789 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005790 return 1
5791
5792 webbrowser.open(issue_url)
5793 return 0
5794
5795
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005796@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005797def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005798 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005799 parser.add_option('-d', '--dry-run', action='store_true',
5800 help='trigger in dry run mode')
5801 parser.add_option('-c', '--clear', action='store_true',
5802 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005803 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005804 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005805 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005806 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005807 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005808 if args:
5809 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005810 if options.dry_run and options.clear:
5811 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5812
iannuccie53c9352016-08-17 14:40:40 -07005813 cl = Changelist(auth_config=auth_config, issue=options.issue,
5814 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005815 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005816 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005817 elif options.dry_run:
5818 state = _CQState.DRY_RUN
5819 else:
5820 state = _CQState.COMMIT
5821 if not cl.GetIssue():
5822 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005823 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005824 return 0
5825
5826
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005827@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005828def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005829 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005830 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005831 auth.add_auth_options(parser)
5832 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005833 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005834 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005835 if args:
5836 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005837 cl = Changelist(auth_config=auth_config, issue=options.issue,
5838 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005839 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005840 if not cl.GetIssue():
5841 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005842 cl.CloseIssue()
5843 return 0
5844
5845
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005846@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005847def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005848 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005849 parser.add_option(
5850 '--stat',
5851 action='store_true',
5852 dest='stat',
5853 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005854 auth.add_auth_options(parser)
5855 options, args = parser.parse_args(args)
5856 auth_config = auth.extract_auth_config_from_options(options)
5857 if args:
5858 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005859
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005860 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005861 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005862 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005863 if not issue:
5864 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005865
Aaron Gablea718c3e2017-08-28 17:47:28 -07005866 base = cl._GitGetBranchConfigValue('last-upload-hash')
5867 if not base:
5868 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5869 if not base:
5870 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5871 revision_info = detail['revisions'][detail['current_revision']]
5872 fetch_info = revision_info['fetch']['http']
5873 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5874 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005875
Aaron Gablea718c3e2017-08-28 17:47:28 -07005876 cmd = ['git', 'diff']
5877 if options.stat:
5878 cmd.append('--stat')
5879 cmd.append(base)
5880 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005881
5882 return 0
5883
5884
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005885@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005886def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005887 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005888 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005889 '--ignore-current',
5890 action='store_true',
5891 help='Ignore the CL\'s current reviewers and start from scratch.')
5892 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005893 '--no-color',
5894 action='store_true',
5895 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005896 parser.add_option(
5897 '--batch',
5898 action='store_true',
5899 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005900 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005901 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005902 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005903
5904 author = RunGit(['config', 'user.email']).strip() or None
5905
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005906 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005907
5908 if args:
5909 if len(args) > 1:
5910 parser.error('Unknown args')
5911 base_branch = args[0]
5912 else:
5913 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005914 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005915
5916 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005917 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5918
5919 if options.batch:
5920 db = owners.Database(change.RepositoryRoot(), file, os.path)
5921 print('\n'.join(db.reviewers_for(affected_files, author)))
5922 return 0
5923
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005924 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005925 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005926 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005927 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005928 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005929 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005930 disable_color=options.no_color,
5931 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005932
5933
Aiden Bennerc08566e2018-10-03 17:52:42 +00005934def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005935 """Generates a diff command."""
5936 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005937 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5938
5939 if not allow_prefix:
5940 diff_cmd += ['--no-prefix']
5941
5942 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005943
5944 if args:
5945 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005946 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005947 diff_cmd.append(arg)
5948 else:
5949 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005950
5951 return diff_cmd
5952
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005953
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005954def MatchingFileType(file_name, extensions):
5955 """Returns true if the file name ends with one of the given extensions."""
5956 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005957
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005958
enne@chromium.org555cfe42014-01-29 18:21:39 +00005959@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005960@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005961def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005962 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005963 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005964 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005965 parser.add_option('--full', action='store_true',
5966 help='Reformat the full content of all touched files')
5967 parser.add_option('--dry-run', action='store_true',
5968 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005969 parser.add_option('--python', action='store_true',
5970 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005971 parser.add_option('--js', action='store_true',
5972 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005973 parser.add_option('--diff', action='store_true',
5974 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005975 parser.add_option('--presubmit', action='store_true',
5976 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005977 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005978
Daniel Chengc55eecf2016-12-30 03:11:02 -08005979 # Normalize any remaining args against the current path, so paths relative to
5980 # the current directory are still resolved as expected.
5981 args = [os.path.join(os.getcwd(), arg) for arg in args]
5982
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005983 # git diff generates paths against the root of the repository. Change
5984 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005985 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005986 if rel_base_path:
5987 os.chdir(rel_base_path)
5988
digit@chromium.org29e47272013-05-17 17:01:46 +00005989 # Grab the merge-base commit, i.e. the upstream commit of the current
5990 # branch when it was created or the last time it was rebased. This is
5991 # to cover the case where the user may have called "git fetch origin",
5992 # moving the origin branch to a newer commit, but hasn't rebased yet.
5993 upstream_commit = None
5994 cl = Changelist()
5995 upstream_branch = cl.GetUpstreamBranch()
5996 if upstream_branch:
5997 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5998 upstream_commit = upstream_commit.strip()
5999
6000 if not upstream_commit:
6001 DieWithError('Could not find base commit for this branch. '
6002 'Are you in detached state?')
6003
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006004 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
6005 diff_output = RunGit(changed_files_cmd)
6006 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00006007 # Filter out files deleted by this CL
6008 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006009
Christopher Lamc5ba6922017-01-24 11:19:14 +11006010 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00006011 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006012
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006013 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
6014 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
6015 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006016 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006017
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00006018 top_dir = os.path.normpath(
6019 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
6020
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006021 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6022 # formatted. This is used to block during the presubmit.
6023 return_value = 0
6024
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006025 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006026 # Locate the clang-format binary in the checkout
6027 try:
6028 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07006029 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00006030 DieWithError(e)
6031
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006032 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006033 cmd = [clang_format_tool]
6034 if not opts.dry_run and not opts.diff:
6035 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006036 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006037 if opts.diff:
6038 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006039 else:
6040 env = os.environ.copy()
6041 env['PATH'] = str(os.path.dirname(clang_format_tool))
6042 try:
6043 script = clang_format.FindClangFormatScriptInChromiumTree(
6044 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006045 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006046 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006047
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006048 cmd = [sys.executable, script, '-p0']
6049 if not opts.dry_run and not opts.diff:
6050 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006051
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006052 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6053 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006054
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006055 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6056 if opts.diff:
6057 sys.stdout.write(stdout)
6058 if opts.dry_run and len(stdout) > 0:
6059 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006060
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006061 # Similar code to above, but using yapf on .py files rather than clang-format
6062 # on C/C++ files
Aiden Bennerc08566e2018-10-03 17:52:42 +00006063 if opts.python and python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006064 yapf_tool = gclient_utils.FindExecutable('yapf')
6065 if yapf_tool is None:
6066 DieWithError('yapf not found in PATH')
6067
Aiden Bennerc08566e2018-10-03 17:52:42 +00006068 # If we couldn't find a yapf file we'll default to the chromium style
6069 # specified in depot_tools.
6070 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6071 chromium_default_yapf_style = os.path.join(depot_tools_path,
6072 YAPF_CONFIG_FILENAME)
6073
6074 # Note: yapf still seems to fix indentation of the entire file
6075 # even if line ranges are specified.
6076 # See https://github.com/google/yapf/issues/499
6077 if not opts.full:
6078 py_line_diffs = _ComputeDiffLineRanges(python_diff_files, upstream_commit)
6079
6080 # Used for caching.
6081 yapf_configs = {}
6082 for f in python_diff_files:
6083 # Find the yapf style config for the current file, defaults to depot
6084 # tools default.
6085 yapf_config = _FindYapfConfigFile(
6086 os.path.abspath(f), yapf_configs, top_dir,
6087 chromium_default_yapf_style)
6088
6089 cmd = [yapf_tool, '--style', yapf_config, f]
6090
6091 has_formattable_lines = False
6092 if not opts.full:
6093 # Only run yapf over changed line ranges.
6094 for diff_start, diff_len in py_line_diffs[f]:
6095 diff_end = diff_start + diff_len - 1
6096 # Yapf errors out if diff_end < diff_start but this
6097 # is a valid line range diff for a removal.
6098 if diff_end >= diff_start:
6099 has_formattable_lines = True
6100 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6101 # If all line diffs were removals we have nothing to format.
6102 if not has_formattable_lines:
6103 continue
6104
6105 if opts.diff or opts.dry_run:
6106 cmd += ['--diff']
6107 # Will return non-zero exit code if non-empty diff.
6108 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
6109 if opts.diff:
6110 sys.stdout.write(stdout)
6111 elif len(stdout) > 0:
6112 return_value = 2
6113 else:
6114 cmd += ['-i']
6115 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006116
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006117 # Dart's formatter does not have the nice property of only operating on
6118 # modified chunks, so hard code full.
6119 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006120 try:
6121 command = [dart_format.FindDartFmtToolInChromiumTree()]
6122 if not opts.dry_run and not opts.diff:
6123 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006124 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006125
ppi@chromium.org6593d932016-03-03 15:41:15 +00006126 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006127 if opts.dry_run and stdout:
6128 return_value = 2
6129 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006130 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6131 'found in this checkout. Files in other languages are still '
6132 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006133
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006134 # Format GN build files. Always run on full build files for canonical form.
6135 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006136 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006137 if opts.dry_run or opts.diff:
6138 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006139 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006140 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6141 shell=sys.platform == 'win32',
6142 cwd=top_dir)
6143 if opts.dry_run and gn_ret == 2:
6144 return_value = 2 # Not formatted.
6145 elif opts.diff and gn_ret == 2:
6146 # TODO this should compute and print the actual diff.
6147 print("This change has GN build file diff for " + gn_diff_file)
6148 elif gn_ret != 0:
6149 # For non-dry run cases (and non-2 return values for dry-run), a
6150 # nonzero error code indicates a failure, probably because the file
6151 # doesn't parse.
6152 DieWithError("gn format failed on " + gn_diff_file +
6153 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006154
Ilya Shermane081cbe2017-08-15 17:51:04 -07006155 # Skip the metrics formatting from the global presubmit hook. These files have
6156 # a separate presubmit hook that issues an error if the files need formatting,
6157 # whereas the top-level presubmit script merely issues a warning. Formatting
6158 # these files is somewhat slow, so it's important not to duplicate the work.
6159 if not opts.presubmit:
6160 for xml_dir in GetDirtyMetricsDirs(diff_files):
6161 tool_dir = os.path.join(top_dir, xml_dir)
6162 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6163 if opts.dry_run or opts.diff:
6164 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006165 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006166 if opts.diff:
6167 sys.stdout.write(stdout)
6168 if opts.dry_run and stdout:
6169 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006170
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006171 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006172
Steven Holte2e664bf2017-04-21 13:10:47 -07006173def GetDirtyMetricsDirs(diff_files):
6174 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6175 metrics_xml_dirs = [
6176 os.path.join('tools', 'metrics', 'actions'),
6177 os.path.join('tools', 'metrics', 'histograms'),
6178 os.path.join('tools', 'metrics', 'rappor'),
6179 os.path.join('tools', 'metrics', 'ukm')]
6180 for xml_dir in metrics_xml_dirs:
6181 if any(file.startswith(xml_dir) for file in xml_diff_files):
6182 yield xml_dir
6183
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006184
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006185@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006186@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006187def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006188 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006189 _, args = parser.parse_args(args)
6190
6191 if len(args) != 1:
6192 parser.print_help()
6193 return 1
6194
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006195 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006196 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006197 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006198
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006199 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006200
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006201 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006202 output = RunGit(['config', '--local', '--get-regexp',
6203 r'branch\..*\.%s' % issueprefix],
6204 error_ok=True)
6205 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006206 if issue == target_issue:
6207 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006208
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006209 branches = []
6210 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006211 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006212 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006213 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006214 return 1
6215 if len(branches) == 1:
6216 RunGit(['checkout', branches[0]])
6217 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006218 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006219 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006220 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006221 which = raw_input('Choose by index: ')
6222 try:
6223 RunGit(['checkout', branches[int(which)]])
6224 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006225 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006226 return 1
6227
6228 return 0
6229
6230
maruel@chromium.org29404b52014-09-08 22:58:00 +00006231def CMDlol(parser, args):
6232 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006233 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006234 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6235 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6236 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006237 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006238 return 0
6239
6240
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006241class OptionParser(optparse.OptionParser):
6242 """Creates the option parse and add --verbose support."""
6243 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006244 optparse.OptionParser.__init__(
6245 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006246 self.add_option(
6247 '-v', '--verbose', action='count', default=0,
6248 help='Use 2 times for more debugging info')
6249
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006250 def parse_args(self, args=None, _values=None):
6251 # Create an optparse.Values object that will store only the actual passed
6252 # options, without the defaults.
6253 actual_options = optparse.Values()
6254 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6255 # Create an optparse.Values object with the default options.
6256 options = optparse.Values(self.get_default_values().__dict__)
6257 # Update it with the options passed by the user.
6258 options._update_careful(actual_options.__dict__)
6259 # Store the options passed by the user in an _actual_options attribute.
6260 # We store only the keys, and not the values, since the values can contain
6261 # arbitrary information, which might be PII.
6262 metrics.collector.add('arguments', actual_options.__dict__.keys())
6263
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006264 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006265 logging.basicConfig(
6266 level=levels[min(options.verbose, len(levels) - 1)],
6267 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6268 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006269
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006270 return options, args
6271
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006273def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006274 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006275 print('\nYour python version %s is unsupported, please upgrade.\n' %
6276 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006277 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006278
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006279 # Reload settings.
6280 global settings
6281 settings = Settings()
6282
Edward Lemurad463c92018-07-25 21:31:23 +00006283 if not metrics.DISABLE_METRICS_COLLECTION:
6284 metrics.collector.add('project_urls', [settings.GetViewVCUrl().strip('/+')])
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006285 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006286 dispatcher = subcommand.CommandDispatcher(__name__)
6287 try:
6288 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006289 except auth.AuthenticationError as e:
6290 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006291 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006292 if e.code != 500:
6293 raise
6294 DieWithError(
6295 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6296 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006297 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006298
6299
6300if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006301 # These affect sys.stdout so do it outside of main() to simplify mocks in
6302 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006303 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006304 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006305 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006306 sys.exit(main(sys.argv[1:]))